From 46d140fcc5fc8783c51ef8375925330260af61a4 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Wed, 8 Apr 2026 08:23:17 -0500 Subject: [PATCH] Add Zoom partner plugin (zoom-plugin) Official Zoom plugin with 30 skills covering the Zoom Developer Platform: REST APIs, Meeting SDK, Video SDK, webhooks, OAuth, bots, and MCP workflows. Bundles 3 remote MCP connectors (meetings, docs, whiteboard) hosted at zoom.us. Zoom MCP connector passed internal review (Anthony Bibbs, 2026-04-07). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude-plugin/marketplace.json | 8 + .../zoom-plugin/.claude-plugin/plugin.json | 22 + partner-built/zoom-plugin/.mcp.json | 25 + partner-built/zoom-plugin/AGENTS.md | 39 + partner-built/zoom-plugin/CHANGELOG.md | 12 + partner-built/zoom-plugin/CONNECTORS.md | 49 + partner-built/zoom-plugin/CONTRIBUTING.md | 186 + partner-built/zoom-plugin/LICENSE | 21 + partner-built/zoom-plugin/README.md | 122 + .../skills/build-zoom-bot/SKILL.md | 37 + .../skills/build-zoom-meeting-app/SKILL.md | 38 + .../skills/choose-zoom-approach/SKILL.md | 37 + .../skills/cobrowse-sdk/RUNBOOK.md | 79 + .../zoom-plugin/skills/cobrowse-sdk/SKILL.md | 894 +++ .../concepts/distribution-methods.md | 13 + .../concepts/jwt-authentication.md | 13 + .../concepts/session-lifecycle.md | 13 + .../concepts/two-roles-pattern.md | 43 + .../examples/agent-integration.md | 7 + .../cobrowse-sdk/examples/annotations.md | 7 + .../examples/auto-reconnection.md | 7 + .../cobrowse-sdk/examples/byop-custom-pin.md | 7 + .../examples/customer-integration.md | 7 + .../examples/multi-tab-persistence.md | 7 + .../cobrowse-sdk/examples/privacy-masking.md | 7 + .../cobrowse-sdk/examples/remote-assist.md | 7 + .../cobrowse-sdk/examples/session-events.md | 7 + .../skills/cobrowse-sdk/get-started.md | 554 ++ .../cobrowse-sdk/references/api-official.md | 104 + .../cobrowse-sdk/references/api-reference.md | 5 + .../skills/cobrowse-sdk/references/api.md | 5 + .../references/authorization-official.md | 90 + .../cobrowse-sdk/references/authorization.md | 5 + .../references/environment-variables.md | 20 + .../cobrowse-sdk/references/error-codes.md | 6 + .../references/features-official.md | 111 + .../cobrowse-sdk/references/features.md | 5 + .../references/get-started-official.md | 91 + .../cobrowse-sdk/references/get-started.md | 5 + .../cobrowse-sdk/references/session-events.md | 5 + .../references/settings-reference.md | 6 + .../troubleshooting/browser-compatibility.md | 7 + .../troubleshooting/common-issues.md | 58 + .../cobrowse-sdk/troubleshooting/cors-csp.md | 11 + .../troubleshooting/error-codes.md | 7 + .../skills/contact-center/RUNBOOK.md | 73 + .../skills/contact-center/SKILL.md | 121 + .../skills/contact-center/android/RUNBOOK.md | 64 + .../skills/contact-center/android/SKILL.md | 53 + .../android/concepts/sdk-lifecycle.md | 42 + .../android/examples/service-patterns.md | 68 + .../references/android-reference-map.md | 51 + .../android/troubleshooting/common-issues.md | 44 + .../concepts/architecture-and-lifecycle.md | 63 + .../skills/contact-center/ios/RUNBOOK.md | 64 + .../skills/contact-center/ios/SKILL.md | 52 + .../ios/concepts/sdk-lifecycle.md | 46 + .../ios/examples/service-patterns.md | 72 + .../ios/references/ios-reference-map.md | 48 + .../ios/troubleshooting/common-issues.md | 42 + .../references/environment-variables.md | 25 + .../references/forum-top-questions.md | 82 + .../references/samples-validation.md | 48 + .../versioning-and-compatibility.md | 41 + .../scenarios/high-level-scenarios.md | 59 + .../common-drift-and-breaks.md | 62 + .../skills/contact-center/web/RUNBOOK.md | 63 + .../skills/contact-center/web/SKILL.md | 54 + .../web/concepts/lifecycle-and-events.md | 45 + .../web/examples/app-context-and-state.md | 60 + .../web/references/web-reference-map.md | 54 + .../web/troubleshooting/common-issues.md | 42 + .../skills/debug-zoom-integration/SKILL.md | 42 + .../zoom-plugin/skills/debug-zoom/SKILL.md | 39 + .../skills/design-mcp-workflow/SKILL.md | 31 + .../zoom-plugin/skills/general/RUNBOOK.md | 65 + .../zoom-plugin/skills/general/SKILL.md | 320 + .../skills/general/references/app-types.md | 106 + .../general/references/authentication.md | 82 + .../references/authorization-patterns.md | 562 ++ .../automatic-skill-chaining-rest-webhooks.md | 176 + .../general/references/community-repos.md | 158 + ...stributed-meeting-fallback-architecture.md | 459 ++ .../references/environment-variables.md | 38 + .../references/interview-answer-routing.md | 20 + .../general/references/known-limitations.md | 101 + .../skills/general/references/marketplace.md | 67 + ...ng-webhooks-oauth-refresh-orchestration.md | 173 + .../references/query-routing-playbook.md | 87 + .../references/routing-implementation.md | 247 + .../skills/general/references/scopes.md | 94 + .../references/sdk-logs-troubleshooting.md | 194 + .../general/references/sdk-upgrade-guide.md | 164 + .../references/sdk-upgrade-workflow.md | 144 + .../use-cases/ai-companion-integration.md | 392 ++ .../general/use-cases/ai-integration.md | 224 + .../general/use-cases/apis-vs-mcp-routing.md | 83 + .../use-cases/backend-automation-s2s-oauth.md | 221 + .../general/use-cases/collaborative-apps.md | 89 + ...ter-app-lifecycle-and-context-switching.md | 36 + .../use-cases/contact-center-integration.md | 39 + .../use-cases/custom-meeting-ui-web.md | 96 + .../skills/general/use-cases/custom-video.md | 232 + .../use-cases/customer-support-cobrowsing.md | 453 ++ .../use-cases/electron-meeting-embed.md | 28 + .../general/use-cases/embed-meetings.md | 230 + .../use-cases/enterprise-app-deployment.md | 34 + .../use-cases/flutter-video-sessions.md | 27 + .../use-cases/form-completion-assistant.md | 527 ++ .../general/use-cases/hd-video-resolution.md | 336 + .../use-cases/high-volume-meeting-platform.md | 56 + .../use-cases/immersive-experiences.md | 83 + .../general/use-cases/in-meeting-apps.md | 306 + .../use-cases/marketplace-publishing.md | 384 + .../general/use-cases/meeting-automation.md | 239 + .../skills/general/use-cases/meeting-bots.md | 311 + .../use-cases/meeting-details-with-events.md | 630 ++ .../use-cases/meeting-links-vs-embedding.md | 38 + .../general/use-cases/minutes-calculation.md | 798 +++ .../native-meeting-sdk-multi-platform.md | 34 + .../native-video-sdk-multi-platform.md | 35 + .../general/use-cases/prebuilt-video-ui.md | 307 + .../probe-sdk-preflight-readiness-gate.md | 34 + .../general/use-cases/qss-monitoring.md | 112 + .../skills/general/use-cases/raw-recording.md | 172 + .../use-cases/react-native-meeting-embed.md | 27 + .../use-cases/react-native-video-sessions.md | 28 + .../use-cases/real-time-media-streams.md | 237 + .../use-cases/recording-download-pipeline.md | 308 + .../use-cases/recording-transcription.md | 302 + .../retrieve-meeting-and-subscribe-events.md | 943 +++ .../rivet-event-driven-api-orchestrator.md | 43 + .../use-cases/saas-app-oauth-integration.md | 196 + .../scribe-transcription-pipeline.md | 58 + .../use-cases/sdk-size-optimization.md | 195 + .../general/use-cases/sdk-wrappers-gui.md | 560 ++ .../server-to-server-oauth-with-webhooks.md | 43 + .../general/use-cases/team-chat-llm-bot.md | 265 + .../general/use-cases/testing-development.md | 341 + .../token-and-scope-troubleshooting.md | 71 + .../use-cases/transcription-bot-linux.md | 428 ++ .../use-cases/usage-reporting-analytics.md | 307 + .../use-cases/user-and-meeting-creation.md | 512 ++ .../video-sdk-bring-your-own-storage.md | 220 + ...rtual-agent-campaign-web-mobile-wrapper.md | 32 + ...tual-agent-knowledge-base-sync-pipeline.md | 30 + .../general/use-cases/web-sdk-embedding.md | 207 + .../use-cases/zoom-phone-smart-embed-crm.md | 31 + .../zoom-plugin/skills/meeting-sdk/RUNBOOK.md | 72 + .../zoom-plugin/skills/meeting-sdk/SKILL.md | 255 + .../skills/meeting-sdk/android/RUNBOOK.md | 64 + .../skills/meeting-sdk/android/SKILL.md | 46 + .../skills/meeting-sdk/android/android.md | 19 + .../android/concepts/architecture.md | 23 + .../android/concepts/lifecycle-workflow.md | 17 + .../android/examples/join-start-pattern.md | 20 + .../references/android-reference-map.md | 35 + .../references/environment-variables.md | 15 + .../versioning-and-compatibility.md | 17 + .../android/scenarios/high-level-scenarios.md | 19 + .../android/troubleshooting/common-issues.md | 25 + .../skills/meeting-sdk/electron/RUNBOOK.md | 63 + .../skills/meeting-sdk/electron/SKILL.md | 86 + .../electron/concepts/high-level-scenarios.md | 26 + .../electron/concepts/lifecycle-workflow.md | 31 + .../concepts/sdk-architecture-pattern.md | 24 + .../examples/authentication-pattern.md | 20 + .../electron/examples/join-meeting-pattern.md | 15 + .../electron/examples/raw-data-pattern.md | 22 + .../electron/examples/setup-guide.md | 23 + .../electron/references/electron-reference.md | 17 + .../electron/references/module-map.md | 33 + .../electron/troubleshooting/common-issues.md | 25 + .../deprecated-and-contradictions.md | 28 + .../electron/troubleshooting/version-drift.md | 17 + .../skills/meeting-sdk/ios/RUNBOOK.md | 64 + .../skills/meeting-sdk/ios/SKILL.md | 41 + .../meeting-sdk/ios/concepts/architecture.md | 23 + .../ios/concepts/lifecycle-workflow.md | 17 + .../ios/examples/join-start-pattern.md | 20 + .../zoom-plugin/skills/meeting-sdk/ios/ios.md | 18 + .../ios/references/environment-variables.md | 15 + .../ios/references/ios-reference-map.md | 33 + .../versioning-and-compatibility.md | 17 + .../ios/scenarios/high-level-scenarios.md | 19 + .../ios/troubleshooting/common-issues.md | 22 + .../skills/meeting-sdk/linux/RUNBOOK.md | 64 + .../skills/meeting-sdk/linux/SKILL.md | 429 ++ .../linux/concepts/high-level-scenarios.md | 406 ++ .../skills/meeting-sdk/linux/linux.md | 363 + .../meeting-sdk/linux/meeting-sdk-bot.md | 825 +++ .../linux/references/linux-reference.md | 712 ++ .../skills/meeting-sdk/macos/RUNBOOK.md | 64 + .../skills/meeting-sdk/macos/SKILL.md | 41 + .../macos/concepts/architecture.md | 23 + .../macos/concepts/lifecycle-workflow.md | 17 + .../macos/examples/join-start-pattern.md | 20 + .../skills/meeting-sdk/macos/macos.md | 17 + .../macos/references/environment-variables.md | 15 + .../macos/references/macos-reference-map.md | 33 + .../versioning-and-compatibility.md | 17 + .../macos/scenarios/high-level-scenarios.md | 19 + .../macos/troubleshooting/common-issues.md | 22 + .../meeting-sdk/react-native/RUNBOOK.md | 64 + .../skills/meeting-sdk/react-native/SKILL.md | 106 + .../react-native/concepts/architecture.md | 16 + .../concepts/auth-and-token-model.md | 17 + .../concepts/high-level-scenarios.md | 25 + .../concepts/lifecycle-workflow.md | 20 + .../examples/join-meeting-pattern.md | 19 + .../examples/provider-hook-pattern.md | 14 + .../react-native/examples/setup-guide.md | 43 + .../examples/start-meeting-pattern.md | 18 + .../react-native/references/android-setup.md | 15 + .../react-native/references/ios-setup.md | 13 + .../references/native-bridge-notes.md | 16 + .../references/official-sources.md | 14 + .../react-native/references/wrapper-api.md | 30 + .../troubleshooting/common-issues.md | 24 + .../deprecated-and-contradictions.md | 27 + .../troubleshooting/version-drift.md | 17 + .../meeting-sdk/references/ai-companion.md | 225 + .../skills/meeting-sdk/references/android.md | 19 + .../meeting-sdk/references/authorization.md | 89 + .../references/bot-authentication.md | 385 + .../meeting-sdk/references/breakout-rooms.md | 490 ++ .../references/environment-variables.md | 29 + .../references/forum-top-questions.md | 100 + .../skills/meeting-sdk/references/ios.md | 27 + .../skills/meeting-sdk/references/macos.md | 16 + .../references/multiple-meetings.md | 28 + .../references/signature-playbook.md | 45 + .../meeting-sdk/references/triage-intake.md | 52 + .../meeting-sdk/references/troubleshooting.md | 132 + .../skills/meeting-sdk/references/unreal.md | 16 + .../skills/meeting-sdk/references/webinars.md | 349 + .../skills/meeting-sdk/unreal/RUNBOOK.md | 64 + .../skills/meeting-sdk/unreal/SKILL.md | 39 + .../unreal/concepts/architecture.md | 23 + .../unreal/concepts/lifecycle-workflow.md | 14 + .../unreal/examples/join-start-pattern.md | 14 + .../references/environment-variables.md | 15 + .../unreal/references/unreal-reference-map.md | 23 + .../versioning-and-compatibility.md | 19 + .../unreal/scenarios/high-level-scenarios.md | 19 + .../unreal/troubleshooting/common-issues.md | 20 + .../skills/meeting-sdk/unreal/unreal.md | 17 + .../skills/meeting-sdk/web/RUNBOOK.md | 66 + .../skills/meeting-sdk/web/SKILL.md | 1127 +++ .../meeting-sdk/web/client-view/RUNBOOK.md | 64 + .../meeting-sdk/web/client-view/SKILL.md | 620 ++ .../web/client-view/references/README.md | 9 + .../meeting-sdk/web/component-view/RUNBOOK.md | 64 + .../meeting-sdk/web/component-view/SKILL.md | 620 ++ .../web/component-view/references/README.md | 9 + .../web/concepts/browser-support.md | 223 + .../web/concepts/sharedarraybuffer.md | 310 + .../web/examples/client-view-basic.md | 12 + .../web/examples/component-view-react.md | 12 + .../component-view-breakout-rooms.md | 37 + .../component-view-ui-customization.md | 56 + .../sharedarraybuffer-gallery-view.md | 32 + .../web/references/web-performance-cpu.md | 26 + .../web-timeout-browser-restriction.md | 33 + .../web/references/web-tracking-id.md | 57 + .../skills/meeting-sdk/web/references/web.md | 309 + .../web/troubleshooting/common-issues.md | 471 ++ .../web/troubleshooting/error-codes.md | 275 + .../skills/meeting-sdk/windows/RUNBOOK.md | 64 + .../skills/meeting-sdk/windows/SKILL.md | 1193 ++++ .../concepts/custom-ui-architecture.md | 224 + .../windows/concepts/custom-ui-vs-raw-data.md | 180 + .../concepts/sdk-architecture-pattern.md | 654 ++ .../windows/concepts/singleton-hierarchy.md | 404 ++ .../windows/examples/ai-companion.md | 539 ++ .../examples/authentication-pattern.md | 702 ++ .../windows/examples/breakout-rooms.md | 552 ++ .../examples/captions-transcription.md | 551 ++ .../meeting-sdk/windows/examples/chat.md | 413 ++ .../examples/custom-ui-video-rendering.md | 237 + .../windows/examples/local-recording.md | 580 ++ .../windows/examples/raw-video-capture.md | 814 +++ .../windows/examples/send-raw-data.md | 478 ++ .../windows/examples/service-quality.md | 249 + .../examples/share-raw-data-capture.md | 541 ++ .../windows/examples/video-advanced.md | 343 + .../windows/references/deployment.md | 260 + .../windows/references/interface-methods.md | 489 ++ .../windows/references/windows-reference.md | 790 +++ .../windows/troubleshooting/build-errors.md | 409 ++ .../windows/troubleshooting/common-issues.md | 575 ++ .../troubleshooting/windows-message-loop.md | 401 ++ .../zoom-plugin/skills/oauth/RUNBOOK.md | 95 + .../zoom-plugin/skills/oauth/SKILL.md | 901 +++ .../skills/oauth/concepts/oauth-flows.md | 530 ++ .../zoom-plugin/skills/oauth/concepts/pkce.md | 415 ++ .../oauth/concepts/scopes-architecture.md | 244 + .../skills/oauth/concepts/state-parameter.md | 234 + .../skills/oauth/concepts/token-lifecycle.md | 484 ++ .../skills/oauth/examples/device-flow.md | 9 + .../oauth/examples/pkce-implementation.md | 9 + .../skills/oauth/examples/s2s-oauth-basic.md | 9 + .../skills/oauth/examples/s2s-oauth-redis.md | 295 + .../skills/oauth/examples/token-refresh.md | 9 + .../skills/oauth/examples/user-oauth-basic.md | 9 + .../skills/oauth/examples/user-oauth-mysql.md | 9 + .../skills/oauth/references/classic-scopes.md | 6270 +++++++++++++++++ .../oauth/references/environment-variables.md | 23 + .../oauth/references/granular-scopes.md | 3458 +++++++++ .../skills/oauth/references/oauth-errors.md | 41 + .../oauth/troubleshooting/common-errors.md | 13 + .../troubleshooting/redirect-uri-issues.md | 7 + .../oauth/troubleshooting/scope-issues.md | 7 + .../oauth/troubleshooting/token-issues.md | 7 + .../zoom-plugin/skills/phone/RUNBOOK.md | 47 + .../zoom-plugin/skills/phone/SKILL.md | 85 + .../concepts/architecture-and-lifecycle.md | 57 + .../examples/phone-api-service-pattern.md | 41 + .../smart-embed-postmessage-bridge.md | 49 + .../references/call-handling-patterns.md | 34 + .../phone/references/crm-sample-validation.md | 36 + .../references/deprecations-and-migrations.md | 30 + .../phone/references/environment-variables.md | 26 + .../phone/references/forum-top-questions.md | 95 + .../references/smart-embed-event-contract.md | 37 + .../skills/phone/references/source-map.md | 28 + .../phone/scenarios/high-level-scenarios.md | 33 + .../phone/troubleshooting/common-issues.md | 39 + .../skills/plan-zoom-integration/SKILL.md | 42 + .../skills/plan-zoom-product/SKILL.md | 41 + .../zoom-plugin/skills/probe-sdk/RUNBOOK.md | 67 + .../zoom-plugin/skills/probe-sdk/SKILL.md | 81 + .../concepts/architecture-and-lifecycle.md | 57 + .../examples/comprehensive-network-pattern.md | 40 + .../examples/diagnostic-page-pattern.md | 42 + .../zoom-plugin/skills/probe-sdk/probe-sdk.md | 15 + .../references/environment-variables.md | 23 + .../references/probe-reference-map.md | 43 + .../references/samples-validation.md | 25 + .../skills/probe-sdk/references/source-map.md | 13 + .../versioning-and-compatibility.md | 24 + .../scenarios/high-level-scenarios.md | 31 + .../troubleshooting/common-issues.md | 65 + .../zoom-plugin/skills/rest-api/RUNBOOK.md | 89 + .../zoom-plugin/skills/rest-api/SKILL.md | 594 ++ .../rest-api/concepts/api-architecture.md | 307 + .../rest-api/concepts/authentication-flows.md | 295 + .../concepts/meeting-urls-and-sdk-joining.md | 38 + .../concepts/rate-limiting-strategy.md | 347 + .../rest-api/examples/graphql-queries.md | 14 + .../rest-api/examples/meeting-lifecycle.md | 599 ++ .../rest-api/examples/recording-pipeline.md | 17 + .../rest-api/examples/user-management.md | 17 + .../rest-api/examples/webhook-server.md | 589 ++ .../skills/rest-api/references/accounts.md | 125 + .../rest-api/references/ai-companion.md | 37 + .../skills/rest-api/references/ai-services.md | 43 + .../rest-api/references/authentication.md | 438 ++ .../skills/rest-api/references/auto-dialer.md | 62 + .../skills/rest-api/references/calendar.md | 100 + .../skills/rest-api/references/chatbot.md | 40 + .../skills/rest-api/references/clips.md | 85 + .../rest-api/references/cobrowse-sdk-api.md | 40 + .../skills/rest-api/references/commerce.md | 111 + .../rest-api/references/contact-center.md | 458 ++ .../skills/rest-api/references/crc.md | 80 + .../references/environment-variables.md | 21 + .../skills/rest-api/references/events.md | 222 + .../skills/rest-api/references/graphql.md | 484 ++ .../skills/rest-api/references/healthcare.md | 39 + .../skills/rest-api/references/mail.md | 131 + .../rest-api/references/marketplace-apps.md | 75 + .../skills/rest-api/references/meetings.md | 327 + .../rest-api/references/number-management.md | 94 + .../skills/rest-api/references/openapi.md | 166 + .../skills/rest-api/references/phone.md | 666 ++ .../skills/rest-api/references/qss.md | 45 + .../rest-api/references/quality-management.md | 48 + .../skills/rest-api/references/rate-limits.md | 386 + .../skills/rest-api/references/recordings.md | 70 + .../skills/rest-api/references/reports.md | 86 + .../references/revenue-accelerator.md | 141 + .../skills/rest-api/references/rooms.md | 211 + .../skills/rest-api/references/scheduler.md | 105 + .../skills/rest-api/references/scim2.md | 53 + .../skills/rest-api/references/tasks.md | 68 + .../skills/rest-api/references/team-chat.md | 233 + .../skills/rest-api/references/users.md | 125 + .../rest-api/references/video-management.md | 85 + .../rest-api/references/video-sdk-api.md | 92 + .../rest-api/references/virtual-agent.md | 54 + .../skills/rest-api/references/webinars.md | 76 + .../skills/rest-api/references/whiteboard.md | 110 + .../references/workforce-management.md | 72 + .../skills/rest-api/references/zoom-docs.md | 82 + .../rest-api/troubleshooting/common-errors.md | 429 ++ .../rest-api/troubleshooting/common-issues.md | 25 + .../troubleshooting/forum-top-questions.md | 56 + .../troubleshooting/token-scope-playbook.md | 48 + .../zoom-plugin/skills/rivet-sdk/RUNBOOK.md | 70 + .../zoom-plugin/skills/rivet-sdk/SKILL.md | 99 + .../concepts/architecture-and-lifecycle.md | 82 + .../examples/getting-started-pattern.md | 43 + .../examples/multi-client-pattern.md | 53 + .../references/environment-variables.md | 44 + .../references/rivet-reference-map.md | 41 + .../references/samples-validation.md | 41 + .../skills/rivet-sdk/references/source-map.md | 19 + .../versioning-and-compatibility.md | 32 + .../zoom-plugin/skills/rivet-sdk/rivet-sdk.md | 15 + .../scenarios/high-level-scenarios.md | 60 + .../troubleshooting/common-issues.md | 51 + .../zoom-plugin/skills/rtms/RUNBOOK.md | 83 + .../zoom-plugin/skills/rtms/SKILL.md | 580 ++ .../rtms/concepts/connection-architecture.md | 223 + .../skills/rtms/concepts/lifecycle-flow.md | 494 ++ .../skills/rtms/examples/ai-integration.md | 436 ++ .../skills/rtms/examples/manual-websocket.md | 580 ++ .../skills/rtms/examples/rtms-bot.md | 696 ++ .../skills/rtms/examples/sdk-quickstart.md | 365 + .../skills/rtms/references/connection.md | 276 + .../skills/rtms/references/data-types.md | 322 + .../rtms/references/environment-variables.md | 26 + .../skills/rtms/references/media-types.md | 240 + .../skills/rtms/references/quickstart.md | 215 + .../skills/rtms/references/webhooks.md | 275 + .../rtms/troubleshooting/common-issues.md | 404 ++ .../zoom-plugin/skills/scribe/RUNBOOK.md | 88 + .../zoom-plugin/skills/scribe/SKILL.md | 117 + .../concepts/auth-and-processing-modes.md | 95 + .../scribe/examples/batch-webhook-pipeline.md | 65 + .../skills/scribe/examples/fast-mode-node.md | 64 + .../skills/scribe/references/api-reference.md | 126 + .../references/environment-variables.md | 41 + .../scribe/references/samples-validation.md | 45 + .../scribe/references/versioning-and-drift.md | 56 + .../scribe/scenarios/high-level-scenarios.md | 86 + .../common-drift-and-breaks.md | 134 + .../skills/setup-zoom-mcp/SKILL.md | 38 + .../skills/setup-zoom-oauth/SKILL.md | 38 + .../zoom-plugin/skills/start/SKILL.md | 46 + .../zoom-plugin/skills/team-chat/RUNBOOK.md | 85 + .../zoom-plugin/skills/team-chat/SKILL.md | 687 ++ .../team-chat/concepts/api-selection.md | 231 + .../team-chat/concepts/authentication.md | 36 + .../skills/team-chat/concepts/deployment.md | 23 + .../team-chat/concepts/environment-setup.md | 301 + .../team-chat/concepts/message-structure.md | 22 + .../skills/team-chat/concepts/security.md | 18 + .../skills/team-chat/concepts/webhooks.md | 493 ++ .../team-chat/examples/button-actions.md | 18 + .../team-chat/examples/channel-management.md | 12 + .../team-chat/examples/chatbot-setup.md | 520 ++ .../examples/database-integration.md | 14 + .../team-chat/examples/dropdown-selects.md | 14 + .../team-chat/examples/form-submissions.md | 15 + .../team-chat/examples/llm-integration.md | 16 + .../examples/multi-step-workflows.md | 13 + .../skills/team-chat/examples/oauth-setup.md | 49 + .../team-chat/examples/scheduled-alerts.md | 14 + .../skills/team-chat/examples/send-message.md | 23 + .../team-chat/examples/slash-commands.md | 16 + .../team-chat/examples/token-management.md | 20 + .../skills/team-chat/get-started.md | 60 + .../team-chat/references/api-reference.md | 23 + .../references/environment-variables.md | 21 + .../team-chat/references/error-codes.md | 22 + .../team-chat/references/jid-formats.md | 10 + .../team-chat/references/message-cards.md | 343 + .../team-chat/references/rate-limits.md | 8 + .../team-chat/references/sample-comparison.md | 18 + .../skills/team-chat/references/samples.md | 547 ++ .../skills/team-chat/references/scopes.md | 17 + .../team-chat/references/webhook-events.md | 17 + .../troubleshooting/common-issues.md | 428 ++ .../troubleshooting/deployment-issues.md | 19 + .../troubleshooting/message-issues.md | 13 + .../team-chat/troubleshooting/oauth-issues.md | 25 + .../troubleshooting/webhook-issues.md | 13 + .../zoom-plugin/skills/ui-toolkit/RUNBOOK.md | 63 + .../zoom-plugin/skills/ui-toolkit/SKILL.md | 555 ++ .../references/environment-variables.md | 19 + .../troubleshooting/common-issues.md | 41 + .../zoom-plugin/skills/video-sdk/RUNBOOK.md | 78 + .../zoom-plugin/skills/video-sdk/SKILL.md | 372 + .../skills/video-sdk/android/RUNBOOK.md | 64 + .../skills/video-sdk/android/SKILL.md | 38 + .../skills/video-sdk/android/android.md | 32 + .../android/concepts/architecture.md | 19 + .../android/concepts/lifecycle-workflow.md | 21 + .../android/examples/session-join-pattern.md | 27 + .../references/android-reference-map.md | 19 + .../references/environment-variables.md | 13 + .../versioning-and-compatibility.md | 18 + .../android/scenarios/high-level-scenarios.md | 26 + .../android/troubleshooting/common-issues.md | 21 + .../skills/video-sdk/flutter/RUNBOOK.md | 64 + .../skills/video-sdk/flutter/SKILL.md | 89 + .../flutter/concepts/high-level-scenarios.md | 21 + .../flutter/concepts/lifecycle-workflow.md | 23 + .../concepts/sdk-architecture-pattern.md | 23 + .../examples/event-handling-pattern.md | 36 + .../flutter/examples/session-join-pattern.md | 21 + .../video-sdk/flutter/examples/setup-guide.md | 86 + .../flutter/references/flutter-reference.md | 12 + .../flutter/references/module-map.md | 20 + .../flutter/references/official-sources.md | 13 + .../flutter/troubleshooting/common-issues.md | 48 + .../deprecated-and-contradictions.md | 27 + .../flutter/troubleshooting/version-drift.md | 11 + .../skills/video-sdk/ios/RUNBOOK.md | 64 + .../zoom-plugin/skills/video-sdk/ios/SKILL.md | 39 + .../video-sdk/ios/concepts/architecture.md | 17 + .../ios/concepts/lifecycle-workflow.md | 20 + .../ios/examples/session-join-pattern.md | 24 + .../zoom-plugin/skills/video-sdk/ios/ios.md | 31 + .../ios/references/environment-variables.md | 13 + .../ios/references/ios-reference-map.md | 18 + .../versioning-and-compatibility.md | 18 + .../ios/scenarios/high-level-scenarios.md | 26 + .../ios/troubleshooting/common-issues.md | 21 + .../skills/video-sdk/linux/RUNBOOK.md | 64 + .../skills/video-sdk/linux/SKILL.md | 443 ++ .../linux/concepts/raw-data-vs-canvas.md | 571 ++ .../concepts/sdk-architecture-pattern.md | 528 ++ .../linux/concepts/singleton-hierarchy.md | 542 ++ .../skills/video-sdk/linux/examples/chat.md | 9 + .../linux/examples/cloud-recording.md | 8 + .../linux/examples/command-channel.md | 321 + .../linux/examples/live-streaming.md | 9 + .../linux/examples/qt-gtk-integration.md | 9 + .../linux/examples/raw-audio-capture.md | 9 + .../linux/examples/raw-video-capture.md | 9 + .../linux/examples/session-join-pattern.md | 642 ++ .../video-sdk/linux/examples/transcription.md | 9 + .../linux/examples/virtual-audio-video.md | 8 + .../skills/video-sdk/linux/linux.md | 787 +++ .../linux/references/linux-reference.md | 800 +++ .../linux/troubleshooting/build-errors.md | 96 + .../linux/troubleshooting/common-issues.md | 192 + .../linux/troubleshooting/pulseaudio-setup.md | 97 + .../linux/troubleshooting/qt-dependencies.md | 108 + .../skills/video-sdk/macos/RUNBOOK.md | 64 + .../skills/video-sdk/macos/SKILL.md | 39 + .../video-sdk/macos/concepts/architecture.md | 17 + .../macos/concepts/lifecycle-workflow.md | 21 + .../macos/examples/session-join-pattern.md | 24 + .../skills/video-sdk/macos/macos.md | 31 + .../macos/references/environment-variables.md | 13 + .../macos/references/macos-reference-map.md | 18 + .../versioning-and-compatibility.md | 18 + .../macos/scenarios/high-level-scenarios.md | 26 + .../macos/troubleshooting/common-issues.md | 21 + .../skills/video-sdk/react-native/RUNBOOK.md | 64 + .../skills/video-sdk/react-native/SKILL.md | 85 + .../concepts/high-level-scenarios.md | 21 + .../concepts/lifecycle-workflow.md | 22 + .../concepts/sdk-architecture-pattern.md | 23 + .../examples/event-handling-pattern.md | 28 + .../examples/session-join-pattern.md | 21 + .../react-native/examples/setup-guide.md | 56 + .../react-native/references/module-map.md | 20 + .../references/official-sources.md | 13 + .../references/react-native-reference.md | 15 + .../troubleshooting/common-issues.md | 56 + .../deprecated-and-contradictions.md | 33 + .../troubleshooting/version-drift.md | 11 + .../video-sdk/references/authorization.md | 121 + .../references/environment-variables.md | 25 + .../references/forum-top-questions.md | 64 + .../references/licensing-and-entitlements.md | 26 + .../video-sdk/references/session-lifecycle.md | 36 + .../references/token-contract-test-spec.md | 46 + .../video-sdk/references/triage-intake.md | 41 + .../video-sdk/references/troubleshooting.md | 59 + .../skills/video-sdk/references/ui-toolkit.md | 208 + .../skills/video-sdk/unity/RUNBOOK.md | 64 + .../skills/video-sdk/unity/SKILL.md | 39 + .../video-sdk/unity/concepts/architecture.md | 18 + .../unity/concepts/lifecycle-workflow.md | 21 + .../unity/examples/session-join-pattern.md | 21 + .../unity/references/environment-variables.md | 13 + .../unity/references/unity-reference-map.md | 18 + .../versioning-and-compatibility.md | 17 + .../unity/scenarios/high-level-scenarios.md | 26 + .../unity/troubleshooting/common-issues.md | 21 + .../skills/video-sdk/unity/unity.md | 31 + .../skills/video-sdk/web/RUNBOOK.md | 69 + .../zoom-plugin/skills/video-sdk/web/SKILL.md | 821 +++ .../web/concepts/sdk-architecture-pattern.md | 323 + .../web/concepts/singleton-hierarchy.md | 405 ++ .../skills/video-sdk/web/examples/chat.md | 491 ++ .../video-sdk/web/examples/command-channel.md | 226 + .../video-sdk/web/examples/event-handling.md | 532 ++ .../web/examples/framework-integrations.md | 446 ++ .../video-sdk/web/examples/react-hooks.md | 411 ++ .../video-sdk/web/examples/recording.md | 435 ++ .../video-sdk/web/examples/screen-share.md | 433 ++ .../web/examples/session-join-pattern.md | 389 + .../video-sdk/web/examples/transcription.md | 531 ++ .../video-sdk/web/examples/video-rendering.md | 384 + .../web/references/events-reference.md | 696 ++ .../video-sdk/web/references/web-reference.md | 411 ++ .../skills/video-sdk/web/references/web.md | 1017 +++ .../web/troubleshooting/common-issues.md | 414 ++ .../skills/video-sdk/windows/RUNBOOK.md | 64 + .../skills/video-sdk/windows/SKILL.md | 1019 +++ .../windows/concepts/canvas-vs-raw-data.md | 327 + .../concepts/sdk-architecture-pattern.md | 298 + .../windows/concepts/singleton-hierarchy.md | 492 ++ .../windows/examples/cloud-recording.md | 317 + .../windows/examples/command-channel.md | 330 + .../examples/dotnet-winforms/README.md | 1226 ++++ .../windows/examples/raw-audio-capture.md | 289 + .../windows/examples/raw-video-capture.md | 424 ++ .../examples/screen-share-subscription.md | 571 ++ .../windows/examples/send-raw-audio.md | 343 + .../windows/examples/send-raw-video.md | 368 + .../windows/examples/session-join-pattern.md | 416 ++ .../windows/examples/transcription.md | 405 ++ .../windows/examples/video-rendering.md | 447 ++ .../windows/references/delegate-methods.md | 591 ++ .../video-sdk/windows/references/samples.md | 282 + .../windows/references/windows-reference.md | 1221 ++++ .../windows/troubleshooting/build-errors.md | 321 + .../windows/troubleshooting/common-issues.md | 289 + .../troubleshooting/windows-message-loop.md | 254 + .../skills/video-sdk/windows/windows.md | 46 + .../skills/virtual-agent/RUNBOOK.md | 31 + .../zoom-plugin/skills/virtual-agent/SKILL.md | 73 + .../skills/virtual-agent/android/SKILL.md | 41 + .../android/concepts/webview-lifecycle.md | 9 + .../android/examples/js-bridge-patterns.md | 37 + .../references/android-reference-map.md | 14 + .../android/troubleshooting/common-issues.md | 21 + .../concepts/architecture-and-lifecycle.md | 27 + .../skills/virtual-agent/ios/SKILL.md | 41 + .../ios/concepts/webview-lifecycle.md | 9 + .../ios/examples/js-bridge-patterns.md | 32 + .../ios/references/ios-reference-map.md | 14 + .../ios/troubleshooting/common-issues.md | 20 + .../references/environment-variables.md | 22 + .../references/samples-validation.md | 22 + .../references/versioning-and-drift.md | 21 + .../scenarios/high-level-scenarios.md | 30 + .../common-drift-and-breaks.md | 40 + .../skills/virtual-agent/web/SKILL.md | 39 + .../web/concepts/lifecycle-and-events.md | 30 + .../examples/campaign-and-entry-patterns.md | 37 + .../web/references/web-reference-map.md | 14 + .../web/troubleshooting/common-issues.md | 21 + .../zoom-plugin/skills/webhooks/RUNBOOK.md | 83 + .../zoom-plugin/skills/webhooks/SKILL.md | 117 + .../references/environment-variables.md | 14 + .../skills/webhooks/references/events.md | 75 + .../references/signature-verification.md | 6 + .../webhooks/references/subscriptions.md | 272 + .../webhooks/references/verification.md | 94 + .../webhooks/troubleshooting/common-issues.md | 34 + .../zoom-plugin/skills/websockets/RUNBOOK.md | 85 + .../zoom-plugin/skills/websockets/SKILL.md | 249 + .../websockets/references/connection.md | 435 ++ .../references/environment-variables.md | 18 + .../skills/websockets/references/events.md | 522 ++ .../troubleshooting/common-issues.md | 34 + .../skills/zoom-apps-sdk/RUNBOOK.md | 106 + .../zoom-plugin/skills/zoom-apps-sdk/SKILL.md | 660 ++ .../zoom-apps-sdk/concepts/architecture.md | 222 + .../concepts/meeting-sdk-vs-zoom-apps.md | 34 + .../concepts/running-contexts.md | 190 + .../skills/zoom-apps-sdk/concepts/security.md | 157 + .../examples/app-communication.md | 152 + .../zoom-apps-sdk/examples/breakout-rooms.md | 113 + .../examples/collaborate-mode.md | 171 + .../zoom-apps-sdk/examples/guest-mode.md | 121 + .../zoom-apps-sdk/examples/in-client-oauth.md | 222 + .../zoom-apps-sdk/examples/layers-camera.md | 218 + .../examples/layers-immersive.md | 285 + .../zoom-apps-sdk/examples/quick-start.md | 258 + .../skills/zoom-apps-sdk/references/apis.md | 274 + .../references/environment-variables.md | 24 + .../skills/zoom-apps-sdk/references/events.md | 205 + .../zoom-apps-sdk/references/layers-api.md | 501 ++ .../skills/zoom-apps-sdk/references/oauth.md | 202 + .../zoom-apps-sdk/references/zmail-sdk.md | 214 + .../troubleshooting/common-issues.md | 85 + .../troubleshooting/debugging.md | 150 + .../troubleshooting/forum-top-questions.md | 39 + .../troubleshooting/migration.md | 106 + .../zoom-plugin/skills/zoom-mcp/RUNBOOK.md | 66 + .../zoom-plugin/skills/zoom-mcp/SKILL.md | 229 + .../zoom-mcp/concepts/mcp-architecture.md | 94 + .../skills/zoom-mcp/concepts/oauth-setup.md | 202 + .../zoom-mcp/examples/create-zoom-doc.md | 52 + .../zoom-mcp/examples/meeting-lifecycle.md | 50 + .../zoom-mcp/examples/search-and-act.md | 87 + .../zoom-mcp/examples/transcript-retrieval.md | 106 + .../skills/zoom-mcp/references/error-codes.md | 59 + .../skills/zoom-mcp/references/tools.md | 149 + .../zoom-mcp/troubleshooting/common-errors.md | 130 + .../skills/zoom-mcp/whiteboard/SKILL.md | 97 + .../authentication-and-identifiers.md | 56 + .../zoom-mcp/whiteboard/references/tools.md | 23 + 703 files changed, 114575 insertions(+) create mode 100644 partner-built/zoom-plugin/.claude-plugin/plugin.json create mode 100644 partner-built/zoom-plugin/.mcp.json create mode 100644 partner-built/zoom-plugin/AGENTS.md create mode 100644 partner-built/zoom-plugin/CHANGELOG.md create mode 100644 partner-built/zoom-plugin/CONNECTORS.md create mode 100644 partner-built/zoom-plugin/CONTRIBUTING.md create mode 100644 partner-built/zoom-plugin/LICENSE create mode 100644 partner-built/zoom-plugin/README.md create mode 100644 partner-built/zoom-plugin/skills/build-zoom-bot/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/build-zoom-meeting-app/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/choose-zoom-approach/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/distribution-methods.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/jwt-authentication.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/session-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/two-roles-pattern.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/agent-integration.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/annotations.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/auto-reconnection.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/byop-custom-pin.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/customer-integration.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/multi-tab-persistence.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/privacy-masking.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/remote-assist.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/examples/session-events.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/get-started.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-official.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-reference.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/api.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization-official.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/error-codes.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/features-official.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/features.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started-official.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/session-events.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/references/settings-reference.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/cors-csp.md create mode 100644 partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/error-codes.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/concepts/sdk-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/examples/service-patterns.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/references/android-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/android/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/concepts/architecture-and-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/concepts/sdk-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/examples/service-patterns.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/references/ios-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/ios/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/references/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/references/samples-validation.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/troubleshooting/common-drift-and-breaks.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/concepts/lifecycle-and-events.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/examples/app-context-and-state.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/references/web-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/contact-center/web/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/debug-zoom-integration/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/debug-zoom/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/design-mcp-workflow/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/general/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/general/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/general/references/app-types.md create mode 100644 partner-built/zoom-plugin/skills/general/references/authentication.md create mode 100644 partner-built/zoom-plugin/skills/general/references/authorization-patterns.md create mode 100644 partner-built/zoom-plugin/skills/general/references/automatic-skill-chaining-rest-webhooks.md create mode 100644 partner-built/zoom-plugin/skills/general/references/community-repos.md create mode 100644 partner-built/zoom-plugin/skills/general/references/distributed-meeting-fallback-architecture.md create mode 100644 partner-built/zoom-plugin/skills/general/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/general/references/interview-answer-routing.md create mode 100644 partner-built/zoom-plugin/skills/general/references/known-limitations.md create mode 100644 partner-built/zoom-plugin/skills/general/references/marketplace.md create mode 100644 partner-built/zoom-plugin/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md create mode 100644 partner-built/zoom-plugin/skills/general/references/query-routing-playbook.md create mode 100644 partner-built/zoom-plugin/skills/general/references/routing-implementation.md create mode 100644 partner-built/zoom-plugin/skills/general/references/scopes.md create mode 100644 partner-built/zoom-plugin/skills/general/references/sdk-logs-troubleshooting.md create mode 100644 partner-built/zoom-plugin/skills/general/references/sdk-upgrade-guide.md create mode 100644 partner-built/zoom-plugin/skills/general/references/sdk-upgrade-workflow.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/ai-companion-integration.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/ai-integration.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/apis-vs-mcp-routing.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/backend-automation-s2s-oauth.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/collaborative-apps.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/contact-center-integration.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/custom-meeting-ui-web.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/custom-video.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/customer-support-cobrowsing.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/electron-meeting-embed.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/embed-meetings.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/enterprise-app-deployment.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/flutter-video-sessions.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/form-completion-assistant.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/hd-video-resolution.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/high-volume-meeting-platform.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/immersive-experiences.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/in-meeting-apps.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/marketplace-publishing.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/meeting-automation.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/meeting-bots.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/meeting-details-with-events.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/meeting-links-vs-embedding.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/minutes-calculation.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/native-meeting-sdk-multi-platform.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/native-video-sdk-multi-platform.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/prebuilt-video-ui.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/qss-monitoring.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/raw-recording.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/react-native-meeting-embed.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/react-native-video-sessions.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/real-time-media-streams.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/recording-download-pipeline.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/recording-transcription.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/rivet-event-driven-api-orchestrator.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/saas-app-oauth-integration.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/scribe-transcription-pipeline.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/sdk-size-optimization.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/sdk-wrappers-gui.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/server-to-server-oauth-with-webhooks.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/team-chat-llm-bot.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/testing-development.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/token-and-scope-troubleshooting.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/transcription-bot-linux.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/usage-reporting-analytics.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/user-and-meeting-creation.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/video-sdk-bring-your-own-storage.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/web-sdk-embedding.md create mode 100644 partner-built/zoom-plugin/skills/general/use-cases/zoom-phone-smart-embed-crm.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/android.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/examples/join-start-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/references/android-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/android/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/authentication-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/join-meeting-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/raw-data-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/setup-guide.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/references/electron-reference.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/references/module-map.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/version-drift.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/examples/join-start-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/ios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/references/ios-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/ios/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/concepts/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/linux.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/meeting-sdk-bot.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/linux/references/linux-reference.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/examples/join-start-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/macos.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/references/macos-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/macos/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/setup-guide.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/android-setup.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/ios-setup.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/native-bridge-notes.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/official-sources.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/wrapper-api.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/version-drift.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/ai-companion.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/android.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/authorization.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/bot-authentication.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/breakout-rooms.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/ios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/macos.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/multiple-meetings.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/signature-playbook.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/triage-intake.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/troubleshooting.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/unreal.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/references/webinars.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/examples/join-start-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/unreal-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/unreal/unreal.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/references/README.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/references/README.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/browser-support.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/sharedarraybuffer.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/examples/client-view-basic.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/examples/component-view-react.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-breakout-rooms.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-ui-customization.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-performance-cpu.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-tracking-id.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/references/web.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/error-codes.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/ai-companion.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/authentication-pattern.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/breakout-rooms.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/captions-transcription.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/chat.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/local-recording.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/raw-video-capture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/send-raw-data.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/service-quality.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/share-raw-data-capture.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/video-advanced.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/references/deployment.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/references/interface-methods.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/references/windows-reference.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/build-errors.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md create mode 100644 partner-built/zoom-plugin/skills/oauth/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/oauth/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/oauth/concepts/oauth-flows.md create mode 100644 partner-built/zoom-plugin/skills/oauth/concepts/pkce.md create mode 100644 partner-built/zoom-plugin/skills/oauth/concepts/scopes-architecture.md create mode 100644 partner-built/zoom-plugin/skills/oauth/concepts/state-parameter.md create mode 100644 partner-built/zoom-plugin/skills/oauth/concepts/token-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/device-flow.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/pkce-implementation.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-basic.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-redis.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/token-refresh.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/user-oauth-basic.md create mode 100644 partner-built/zoom-plugin/skills/oauth/examples/user-oauth-mysql.md create mode 100644 partner-built/zoom-plugin/skills/oauth/references/classic-scopes.md create mode 100644 partner-built/zoom-plugin/skills/oauth/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/oauth/references/granular-scopes.md create mode 100644 partner-built/zoom-plugin/skills/oauth/references/oauth-errors.md create mode 100644 partner-built/zoom-plugin/skills/oauth/troubleshooting/common-errors.md create mode 100644 partner-built/zoom-plugin/skills/oauth/troubleshooting/redirect-uri-issues.md create mode 100644 partner-built/zoom-plugin/skills/oauth/troubleshooting/scope-issues.md create mode 100644 partner-built/zoom-plugin/skills/oauth/troubleshooting/token-issues.md create mode 100644 partner-built/zoom-plugin/skills/phone/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/phone/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/phone/concepts/architecture-and-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/phone/examples/phone-api-service-pattern.md create mode 100644 partner-built/zoom-plugin/skills/phone/examples/smart-embed-postmessage-bridge.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/call-handling-patterns.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/crm-sample-validation.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/deprecations-and-migrations.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/smart-embed-event-contract.md create mode 100644 partner-built/zoom-plugin/skills/phone/references/source-map.md create mode 100644 partner-built/zoom-plugin/skills/phone/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/phone/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/plan-zoom-integration/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/plan-zoom-product/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/concepts/architecture-and-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/examples/comprehensive-network-pattern.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/examples/diagnostic-page-pattern.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/probe-sdk.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/references/probe-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/references/samples-validation.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/references/source-map.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/probe-sdk/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/concepts/api-architecture.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/concepts/authentication-flows.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/concepts/rate-limiting-strategy.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/examples/graphql-queries.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/examples/meeting-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/examples/recording-pipeline.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/examples/user-management.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/examples/webhook-server.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/accounts.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/ai-companion.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/ai-services.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/authentication.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/auto-dialer.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/calendar.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/chatbot.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/clips.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/cobrowse-sdk-api.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/commerce.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/contact-center.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/crc.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/events.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/graphql.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/healthcare.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/mail.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/marketplace-apps.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/meetings.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/number-management.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/openapi.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/phone.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/qss.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/quality-management.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/rate-limits.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/recordings.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/reports.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/revenue-accelerator.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/rooms.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/scheduler.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/scim2.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/tasks.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/team-chat.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/users.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/video-management.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/video-sdk-api.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/virtual-agent.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/webinars.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/whiteboard.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/workforce-management.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/references/zoom-docs.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-errors.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/troubleshooting/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/rest-api/troubleshooting/token-scope-playbook.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/concepts/architecture-and-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/examples/getting-started-pattern.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/examples/multi-client-pattern.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/references/rivet-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/references/samples-validation.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/references/source-map.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/rivet-sdk.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/rivet-sdk/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/rtms/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/rtms/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/rtms/concepts/connection-architecture.md create mode 100644 partner-built/zoom-plugin/skills/rtms/concepts/lifecycle-flow.md create mode 100644 partner-built/zoom-plugin/skills/rtms/examples/ai-integration.md create mode 100644 partner-built/zoom-plugin/skills/rtms/examples/manual-websocket.md create mode 100644 partner-built/zoom-plugin/skills/rtms/examples/rtms-bot.md create mode 100644 partner-built/zoom-plugin/skills/rtms/examples/sdk-quickstart.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/connection.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/data-types.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/media-types.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/quickstart.md create mode 100644 partner-built/zoom-plugin/skills/rtms/references/webhooks.md create mode 100644 partner-built/zoom-plugin/skills/rtms/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/scribe/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/scribe/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/scribe/concepts/auth-and-processing-modes.md create mode 100644 partner-built/zoom-plugin/skills/scribe/examples/batch-webhook-pipeline.md create mode 100644 partner-built/zoom-plugin/skills/scribe/examples/fast-mode-node.md create mode 100644 partner-built/zoom-plugin/skills/scribe/references/api-reference.md create mode 100644 partner-built/zoom-plugin/skills/scribe/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/scribe/references/samples-validation.md create mode 100644 partner-built/zoom-plugin/skills/scribe/references/versioning-and-drift.md create mode 100644 partner-built/zoom-plugin/skills/scribe/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/scribe/troubleshooting/common-drift-and-breaks.md create mode 100644 partner-built/zoom-plugin/skills/setup-zoom-mcp/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/setup-zoom-oauth/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/start/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/api-selection.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/authentication.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/deployment.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/environment-setup.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/message-structure.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/security.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/concepts/webhooks.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/button-actions.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/channel-management.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/chatbot-setup.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/database-integration.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/dropdown-selects.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/form-submissions.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/llm-integration.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/multi-step-workflows.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/oauth-setup.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/scheduled-alerts.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/send-message.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/slash-commands.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/examples/token-management.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/get-started.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/api-reference.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/error-codes.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/jid-formats.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/message-cards.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/rate-limits.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/sample-comparison.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/samples.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/scopes.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/references/webhook-events.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/troubleshooting/deployment-issues.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/troubleshooting/message-issues.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/troubleshooting/oauth-issues.md create mode 100644 partner-built/zoom-plugin/skills/team-chat/troubleshooting/webhook-issues.md create mode 100644 partner-built/zoom-plugin/skills/ui-toolkit/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/ui-toolkit/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/ui-toolkit/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/ui-toolkit/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/android.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/references/android-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/android/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/concepts/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/examples/event-handling-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/examples/setup-guide.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/references/flutter-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/references/module-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/references/official-sources.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/troubleshooting/deprecated-and-contradictions.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/flutter/troubleshooting/version-drift.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/ios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/references/ios-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/ios/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/concepts/raw-data-vs-canvas.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/concepts/singleton-hierarchy.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/chat.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/cloud-recording.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/command-channel.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/live-streaming.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/qt-gtk-integration.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/raw-audio-capture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/raw-video-capture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/transcription.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/examples/virtual-audio-video.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/linux.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/references/linux-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/troubleshooting/build-errors.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/troubleshooting/pulseaudio-setup.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/linux/troubleshooting/qt-dependencies.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/macos.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/references/macos-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/macos/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/concepts/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/examples/event-handling-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/examples/setup-guide.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/references/module-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/references/official-sources.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/references/react-native-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/troubleshooting/deprecated-and-contradictions.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/react-native/troubleshooting/version-drift.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/authorization.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/licensing-and-entitlements.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/session-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/token-contract-test-spec.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/triage-intake.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/troubleshooting.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/references/ui-toolkit.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/concepts/lifecycle-workflow.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/references/unity-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/references/versioning-and-compatibility.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/unity/unity.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/concepts/singleton-hierarchy.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/chat.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/command-channel.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/event-handling.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/framework-integrations.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/react-hooks.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/recording.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/screen-share.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/transcription.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/examples/video-rendering.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/references/events-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/references/web-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/references/web.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/web/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/concepts/singleton-hierarchy.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/cloud-recording.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/command-channel.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/dotnet-winforms/README.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-audio-capture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-video-capture.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/screen-share-subscription.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-audio.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-video.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/session-join-pattern.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/transcription.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/examples/video-rendering.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/references/delegate-methods.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/references/samples.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/references/windows-reference.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/build-errors.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/windows-message-loop.md create mode 100644 partner-built/zoom-plugin/skills/video-sdk/windows/windows.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/android/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/android/concepts/webview-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/android/examples/js-bridge-patterns.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/android/references/android-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/android/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/concepts/architecture-and-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/ios/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/ios/concepts/webview-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/ios/examples/js-bridge-patterns.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/ios/references/ios-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/ios/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/references/samples-validation.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/references/versioning-and-drift.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/scenarios/high-level-scenarios.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/web/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/web/concepts/lifecycle-and-events.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/web/references/web-reference-map.md create mode 100644 partner-built/zoom-plugin/skills/virtual-agent/web/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/references/events.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/references/signature-verification.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/references/subscriptions.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/references/verification.md create mode 100644 partner-built/zoom-plugin/skills/webhooks/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/websockets/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/websockets/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/websockets/references/connection.md create mode 100644 partner-built/zoom-plugin/skills/websockets/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/websockets/references/events.md create mode 100644 partner-built/zoom-plugin/skills/websockets/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/architecture.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/running-contexts.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/security.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/app-communication.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/breakout-rooms.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/collaborate-mode.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/guest-mode.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/in-client-oauth.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-camera.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-immersive.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/quick-start.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/apis.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/environment-variables.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/events.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/layers-api.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/oauth.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/references/zmail-sdk.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/common-issues.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/debugging.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md create mode 100644 partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/migration.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/RUNBOOK.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/concepts/mcp-architecture.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/concepts/oauth-setup.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/examples/create-zoom-doc.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/examples/meeting-lifecycle.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/examples/search-and-act.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/examples/transcript-retrieval.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/references/error-codes.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/references/tools.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/troubleshooting/common-errors.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/SKILL.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/authentication-and-identifiers.md create mode 100644 partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/tools.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ecac6274..0357d7f3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -111,6 +111,14 @@ "name": "Tribe AI" } }, + { + "name": "zoom-plugin", + "source": "./partner-built/zoom-plugin", + "description": "Plan, build, and debug Zoom integrations across REST APIs, Meeting SDK, Video SDK, webhooks, bots, and MCP workflows. Search meetings, retrieve recordings, access transcripts, and design AI-powered Zoom experiences.", + "author": { + "name": "Zoom" + } + }, { "name": "planetscale", "description": "An authenticated hosted MCP server that accesses your PlanetScale organizations, databases, branches, schema, and Insights data. Query against your data, surface slow queries, and get organizational and account information.", diff --git a/partner-built/zoom-plugin/.claude-plugin/plugin.json b/partner-built/zoom-plugin/.claude-plugin/plugin.json new file mode 100644 index 00000000..492e79ee --- /dev/null +++ b/partner-built/zoom-plugin/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "zoom-plugin", + "description": "Claude plugin for planning, building, and debugging Zoom integrations across REST APIs, SDKs, webhooks, bots, and MCP workflows", + "version": "1.1.0", + "homepage": "https://developers.zoom.us/", + "repository": "https://github.com/zoom/zoom-plugin", + "license": "MIT", + "keywords": [ + "zoom", + "claude-plugin", + "rest-api", + "meeting-sdk", + "video-sdk", + "webhooks", + "oauth", + "mcp" + ], + "author": { + "name": "Zoom", + "url": "https://github.com/zoom/zoom-plugin" + } +} diff --git a/partner-built/zoom-plugin/.mcp.json b/partner-built/zoom-plugin/.mcp.json new file mode 100644 index 00000000..b35b00ee --- /dev/null +++ b/partner-built/zoom-plugin/.mcp.json @@ -0,0 +1,25 @@ +{ + "mcpServers": { + "zoom-mcp": { + "type": "http", + "url": "https://mcp-us.zoom.us/mcp/zoom/streamable", + "headers": { + "Authorization": "Bearer ${ZOOM_MCP_ACCESS_TOKEN}" + } + }, + "zoom-docs-mcp": { + "type": "http", + "url": "https://mcp.zoom.us/mcp/docs/streamable", + "headers": { + "Authorization": "Bearer ${ZOOM_DOCS_MCP_ACCESS_TOKEN}" + } + }, + "zoom-whiteboard-mcp": { + "type": "http", + "url": "https://mcp-us.zoom.us/mcp/whiteboard/streamable", + "headers": { + "Authorization": "Bearer ${ZOOM_WHITEBOARD_MCP_ACCESS_TOKEN}" + } + } + } +} diff --git a/partner-built/zoom-plugin/AGENTS.md b/partner-built/zoom-plugin/AGENTS.md new file mode 100644 index 00000000..fe666dda --- /dev/null +++ b/partner-built/zoom-plugin/AGENTS.md @@ -0,0 +1,39 @@ +# Zoom Plugin + +Cross-platform discovery file for agent tools that look for `AGENTS.md`. + +## What This Repo Provides + +This repository contains a Zoom developer plugin centered on `SKILL.md`-based workflows and reference material. + +Primary capabilities: +- choose the right Zoom surface for a use case +- plan Zoom integrations across REST APIs, SDKs, webhooks, OAuth, and MCP +- debug broken Zoom integrations +- build focused Zoom implementations for meetings, bots, chat, phone, contact center, and virtual agent workflows +- provide deep product-specific reference material under `skills/` + +## Primary Entry Skills + +- `skills/start/SKILL.md` — default routing entry point +- `skills/plan-zoom-product/SKILL.md` — pick the right Zoom developer product +- `skills/plan-zoom-integration/SKILL.md` — turn an idea into an implementation plan +- `skills/setup-zoom-oauth/SKILL.md` — choose the auth model and redirect flow +- `skills/build-zoom-meeting-app/SKILL.md` — implement an embedded or managed meeting app +- `skills/build-zoom-bot/SKILL.md` — implement a meeting bot or recorder +- `skills/debug-zoom/SKILL.md` — isolate the failing integration layer +- `skills/setup-zoom-mcp/SKILL.md` — plan a Zoom MCP workflow for Claude + +## Repo Shape + +- `.claude-plugin/plugin.json` — Claude plugin manifest +- `.mcp.json` — bundled Zoom MCP server definition +- `skills/` — all plugin skills and supporting references +- `README.md` — user-facing overview +- `CONNECTORS.md` — bundled MCP connector notes + +## Usage Notes + +- For Claude Code, install or load this as a plugin. +- For other agent ecosystems, treat the `skills/` tree as the primary reusable asset. +- Workflow skills are the front door; product-specific folders under `skills/` are supporting references. diff --git a/partner-built/zoom-plugin/CHANGELOG.md b/partner-built/zoom-plugin/CHANGELOG.md new file mode 100644 index 00000000..09c59e3c --- /dev/null +++ b/partner-built/zoom-plugin/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this plugin are documented in this file. + +## Unreleased + +- aligned the repository with the current Claude plugin structure around `.claude-plugin/plugin.json`, `skills/`, and `.mcp.json` +- added Claude-facing installation and connector documentation +- converted command-style workflows into `SKILL.md`-based workflows under `skills/` +- bundled the main Zoom MCP server configuration in `.mcp.json` +- removed the Whiteboard MCP server from the bundled plugin surface +- tightened skill metadata and reduced maintainer-facing wording in user-facing docs diff --git a/partner-built/zoom-plugin/CONNECTORS.md b/partner-built/zoom-plugin/CONNECTORS.md new file mode 100644 index 00000000..27293d2e --- /dev/null +++ b/partner-built/zoom-plugin/CONNECTORS.md @@ -0,0 +1,49 @@ +# Connectors + +This plugin works in two modes: + +- Standalone: Claude uses the bundled Zoom skills and reference material included with this plugin. +- Supercharged: Claude can also use the bundled Zoom MCP servers from [`.mcp.json`](./.mcp.json) for live tool access. + +## Included MCP Servers + +| Connector | Endpoint | Use For | +|---|---|---| +| `zoom-mcp` | `https://mcp-us.zoom.us/mcp/zoom/streamable` | Zoom-hosted MCP workflows for meetings, recordings, summaries, and meeting assets | +| `zoom-docs-mcp` | `https://mcp.zoom.us/mcp/docs/streamable` | Zoom Docs creation, retrieval, and Markdown-based document workflows | +| `zoom-whiteboard-mcp` | `https://mcp-us.zoom.us/mcp/whiteboard/streamable` | Whiteboard-specific MCP workflows | + +## Authentication + +The bundled MCP definitions expect bearer tokens in these environment variables: + +```bash +export ZOOM_MCP_ACCESS_TOKEN="your_zoom_user_oauth_access_token" +export ZOOM_DOCS_MCP_ACCESS_TOKEN="your_zoom_docs_mcp_access_token" +export ZOOM_WHITEBOARD_MCP_ACCESS_TOKEN="your_zoom_user_oauth_access_token" +``` + +- `ZOOM_MCP_ACCESS_TOKEN` is used for the main Zoom MCP server. +- `ZOOM_DOCS_MCP_ACCESS_TOKEN` is used for the Zoom Docs MCP server. +- `ZOOM_WHITEBOARD_MCP_ACCESS_TOKEN` is used for the Whiteboard MCP server. +- If one OAuth token includes both the main Zoom MCP scopes and the Zoom Docs MCP scopes, both variables can use the same value. +- After setting or rotating any of these tokens, restart Claude Code or re-enable the plugin so the MCP servers restart with the new environment. + +## What You Can Do Without Connectors + +- Choose the right Zoom surface for a new integration +- Plan SDK, REST API, webhook, OAuth, and MCP implementations +- Compare Meeting SDK vs Video SDK vs Zoom Apps vs REST API +- Debug architecture, auth, event-delivery, and integration mistakes +- Use the deep Zoom reference library bundled in `skills/` + +## What Connectors Add + +- Live MCP tool discovery and execution against Zoom-hosted MCP servers +- Real meeting-search, recording-resource, and document workflows +- Whiteboard-specific tool access when applicable + +## Notes + +- If a command or skill mentions connectors and you are not connected, continue in standalone mode using the reference docs. +- If you are unsure which connector is relevant, start with [`/setup-zoom-mcp`](./skills/setup-zoom-mcp/SKILL.md). diff --git a/partner-built/zoom-plugin/CONTRIBUTING.md b/partner-built/zoom-plugin/CONTRIBUTING.md new file mode 100644 index 00000000..c0dd6e00 --- /dev/null +++ b/partner-built/zoom-plugin/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# Contributing to Zoom Developer Platform Agent Skills + +Thank you for your interest in contributing! This document provides guidelines for contributing to these Agent Skills. + +## Ways to Contribute + +You can contribute in any of these ways: + +1. Submit a pull request with improvements. +2. Raise an issue on GitHub for bugs, gaps, or enhancement ideas. +3. Reach out on the [Zoom Developer Forum](https://devforum.zoom.us/) with feedback and improvement suggestions for these agent skills. + +### 1. Report Issues +- Documentation errors or outdated information +- Missing use cases or scenarios +- Incorrect code examples + +### 2. Improve Documentation +- Fix typos and clarify explanations +- Add missing code examples +- Update for new SDK versions + +### 3. Add New Skills +- New use cases +- New platform coverage +- New integration patterns + +## Contribution Process + +### For Small Changes (typos, clarifications) + +1. Fork the repository +2. Make your changes +3. Submit a pull request with a clear description + +### For Larger Changes (new use cases, skills) + +1. Open an issue first to discuss the proposed change +2. Fork the repository +3. Create a feature branch +4. Follow the skill format guidelines below +5. Submit a pull request + +## Skill Format Guidelines + +### SKILL.md Structure + +```markdown +--- +name: skill-name +description: | + Brief description (1-3 sentences). + Include when to use this skill. +--- + +# Skill Title + +[Content following the template in PLAN.md] +``` + +### Guidelines + +1. **Keep SKILL.md under 500 lines** - Move details to `references/` +2. **Max 3 directory levels** - `skill/references/file.md` +3. **Include code examples** - Real, working code developers can use +4. **Document gotchas** - Common mistakes and limitations +5. **Link to official sources** - Prefer Zoom documentation + +### Maintenance Checklist + +Use this checklist before merging documentation or skill changes: + +1. Confirm you are editing the correct skill or product folder. +2. Keep `SKILL.md` as the entrypoint in every skill directory. +3. If examples include credentials, reference `.env` keys rather than hardcoded values. +4. Never commit machine-local absolute paths or machine-specific endpoints. +5. After moving or renaming docs, update cross-links from the relevant parent `SKILL.md` files. +6. Verify frontmatter stays accurate: `name`, `description`, and any optional fields such as `triggers`, `argument-hint`, or `user-invocable`. +7. Remove dead links and stale product claims after any refactor or version update. +8. Make sure every new markdown file is reachable from at least one parent navigation file. +9. Track deprecations and renames explicitly so future updates remain migration-safe. + +### Repository Naming Conventions + +- Keep canonical skill folder names aligned with [skills/start/SKILL.md](skills/start/SKILL.md). +- Current canonical folders include: +- `general`, `rest-api`, `webhooks`, `websockets`, `meeting-sdk`, `video-sdk`, `zoom-apps-sdk` +- `rtms`, `team-chat`, `ui-toolkit`, `cobrowse-sdk`, `oauth`, `zoom-mcp` +- `contact-center`, `virtual-agent`, `phone`, `rivet-sdk`, `probe-sdk` + +### Markdown Linking Rules (Required) + +- Use real markdown links for local docs (for example: `text -> docs/example.md`). +- Do not use backticks for local doc references if you want them counted in relationship graphs. +- Use repository-relative paths; do not commit machine-local absolute paths (for example `/home/your-user/...`). +- Every new `.md` file should be linked from at least one parent/index/`SKILL.md` file. + +## Using Claude for Contributions + +You can use Claude (or other AI assistants) to help create or improve skills: + +### Recommended Workflow + +1. **Research Phase** + ``` + Research the official Zoom documentation for [topic]. + Check the developer forum for common issues. + Find working code examples. + ``` + +2. **Drafting Phase** + ``` + Create a skill following the SKILL.md template. + Include practical code examples. + Document known limitations and gotchas. + ``` + +3. **Validation Phase** + ``` + Cross-check all information with official Zoom docs. + Verify code examples are syntactically correct. + Ensure links are valid. + ``` + +### Claude-Specific Tips + +- **Be specific**: "Create a use case for RTMS audio streaming to S3" not "write about RTMS" +- **Provide context**: Share relevant existing skills as examples +- **Iterate**: Review drafts and ask for improvements +- **Verify**: Always cross-check AI-generated content with official sources + +### What Claude Can Help With + +| Task | How Claude Helps | +|------|------------------| +| Research | Search docs, forums, GitHub for information | +| Drafting | Create initial skill content following templates | +| Code examples | Generate working code snippets | +| Cross-referencing | Check consistency across skills | +| Formatting | Ensure markdown is correct | + +### What Requires Human Review + +| Task | Why Human Review | +|------|------------------| +| Technical accuracy | AI may hallucinate APIs or features | +| Real-world gotchas | Comes from actual development experience | +| Business logic | Zoom-specific requirements and policies | +| Security practices | Must be verified against official guidance | + +## Quality Standards + +### Do + +- Verify all claims with official documentation +- Include working, tested code examples +- Document known limitations prominently +- Link to official resources +- Keep examples simple and practical +- Check that moved or renamed docs still have inbound links +- Remove outdated guidance that no longer matches the current plugin structure + +### Don't + +- Include unverified information +- Speculate about undocumented behavior +- Copy proprietary code without permission +- Include outdated or deprecated APIs without noting it +- Over-engineer examples + +## Code of Conduct + +- Be respectful and constructive +- Focus on improving the documentation +- Credit sources appropriately +- Follow Zoom's developer terms of service + +## Questions? + +- Open a GitHub issue for questions about contributing +- Check existing issues before creating new ones +- Join the [Zoom Developer Forum](https://devforum.zoom.us/) for Zoom-specific questions, feedback, and improvement requests for these agent skills + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/partner-built/zoom-plugin/LICENSE b/partner-built/zoom-plugin/LICENSE new file mode 100644 index 00000000..d95e1f8a --- /dev/null +++ b/partner-built/zoom-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Zoom Video Communications, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/partner-built/zoom-plugin/README.md b/partner-built/zoom-plugin/README.md new file mode 100644 index 00000000..55200367 --- /dev/null +++ b/partner-built/zoom-plugin/README.md @@ -0,0 +1,122 @@ +# Zoom Plugin + +A Claude plugin for planning, building, and debugging Zoom integrations. It helps choose the right Zoom surface, shape implementations, debug failures, and route into the right Zoom references without making the user read the whole doc tree first. + +## Installation + +Install this directory as a local Claude plugin. The plugin manifest is at [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) and the bundled Zoom MCP connectors are defined in [`.mcp.json`](.mcp.json). + +Before using the bundled MCP servers, export bearer tokens for the Zoom surfaces you want Claude to use: + +```bash +export ZOOM_MCP_ACCESS_TOKEN="your_zoom_user_oauth_access_token" +export ZOOM_DOCS_MCP_ACCESS_TOKEN="your_zoom_docs_mcp_access_token" +export ZOOM_WHITEBOARD_MCP_ACCESS_TOKEN="your_zoom_user_oauth_access_token" +``` + +## Slash Workflows + +Explicit slash workflows implemented as skills under `skills/`: + +| Workflow | Description | +|---|---| +| [`/start`](skills/start/SKILL.md) | Start with a Zoom app idea and get routed to the right product and build path | +| [`/setup-zoom-oauth`](skills/setup-zoom-oauth/SKILL.md) | Choose the auth model, scopes, and redirect flow for a Zoom app | +| [`/build-zoom-meeting-app`](skills/build-zoom-meeting-app/SKILL.md) | Build an embedded or managed Zoom meeting flow | +| [`/build-zoom-bot`](skills/build-zoom-bot/SKILL.md) | Build bots, recorders, and real-time meeting processors | +| [`/debug-zoom`](skills/debug-zoom/SKILL.md) | Triage a broken Zoom integration and isolate the failing layer | +| [`/setup-zoom-mcp`](skills/setup-zoom-mcp/SKILL.md) | Decide when Zoom MCP fits and set up a safe Claude workflow | +| [`/build-zoom-rest-api-app`](skills/rest-api/SKILL.md) | Route into Zoom REST endpoints, scopes, and resource patterns | +| [`/build-zoom-meeting-sdk-app`](skills/meeting-sdk/SKILL.md) | Route into embedded Zoom meeting implementation details | +| [`/build-zoom-video-sdk-app`](skills/video-sdk/SKILL.md) | Route into custom video-session implementation details | +| [`/setup-zoom-webhooks`](skills/webhooks/SKILL.md) | Set up Zoom webhook subscriptions, signature verification, and handlers | +| [`/setup-zoom-websockets`](skills/websockets/SKILL.md) | Set up Zoom WebSocket event delivery when it fits better than webhooks | +| [`/build-zoom-team-chat-app`](skills/team-chat/SKILL.md) | Build Team Chat user or chatbot integrations | +| [`/build-zoom-phone-integration`](skills/phone/SKILL.md) | Build Zoom Phone integrations around Smart Embed, APIs, and events | +| [`/build-zoom-contact-center-app`](skills/contact-center/SKILL.md) | Build Contact Center app, web, or native integrations | +| [`/build-zoom-virtual-agent`](skills/virtual-agent/SKILL.md) | Build Virtual Agent web or mobile wrapper integrations | + +## Internal Routing Skills + +These remain in the plugin as automatic routing helpers, but they are no longer part of the public slash-command surface: + +- [`start`](skills/start/SKILL.md) +- [`plan-zoom-product`](skills/plan-zoom-product/SKILL.md) +- [`plan-zoom-integration`](skills/plan-zoom-integration/SKILL.md) +- [`choose-zoom-approach`](skills/choose-zoom-approach/SKILL.md) +- [`design-mcp-workflow`](skills/design-mcp-workflow/SKILL.md) +- [`debug-zoom-integration`](skills/debug-zoom-integration/SKILL.md) + +## Deep References + +The plugin also keeps the original Zoom product-specific reference library under `skills/`. These are supporting references, not the primary entry surface: + +- [`skills/general/`](skills/general/) +- [`skills/rest-api/`](skills/rest-api/) +- [`skills/meeting-sdk/`](skills/meeting-sdk/) +- [`skills/video-sdk/`](skills/video-sdk/) +- [`skills/webhooks/`](skills/webhooks/) +- [`skills/websockets/`](skills/websockets/) +- [`skills/oauth/`](skills/oauth/) +- [`skills/zoom-mcp/`](skills/zoom-mcp/) + +## Example Workflows + +### Starting from a Zoom app idea + +```text +/start Build an internal meeting assistant that joins calls, extracts action items, and stores summaries +``` + +### Planning a new app + +```text +/start Build a React app that lets customers schedule and join Zoom meetings from our product +``` + +### Debugging a broken webhook + +```text +/debug-zoom My Zoom webhook signature verification fails in production but not locally +``` + +### Designing an MCP flow + +```text +/setup-zoom-mcp I want Claude to search meetings, pull recording resources, and create follow-up docs +``` + +## Connectors + +See [CONNECTORS.md](CONNECTORS.md). The plugin works standalone from the bundled skills, and gets supercharged when Claude can use the bundled Zoom MCP servers from [`.mcp.json`](.mcp.json). + +## Cross-Platform Notes + +This repo is packaged first as a Claude plugin, but it also includes [AGENTS.md](AGENTS.md) for agent ecosystems that use a repo-level discovery file. The reusable core remains the `skills/` tree and its `SKILL.md` files. + +## Structure + +```text +Zoom Plugin/ +├── .claude-plugin/plugin.json +├── .mcp.json +├── CONNECTORS.md +├── skills/ +│ ├── plan-zoom-product/ +│ ├── plan-zoom-integration/ +│ ├── debug-zoom/ +│ ├── setup-zoom-mcp/ +│ ├── start/ +│ ├── choose-zoom-approach/ +│ ├── setup-zoom-oauth/ +│ ├── build-zoom-meeting-app/ +│ ├── build-zoom-bot/ +│ ├── design-mcp-workflow/ +│ ├── debug-zoom-integration/ +│ └── ... existing Zoom reference skills +└── README.md +``` + +## License + +MIT diff --git a/partner-built/zoom-plugin/skills/build-zoom-bot/SKILL.md b/partner-built/zoom-plugin/skills/build-zoom-bot/SKILL.md new file mode 100644 index 00000000..92a7ea63 --- /dev/null +++ b/partner-built/zoom-plugin/skills/build-zoom-bot/SKILL.md @@ -0,0 +1,37 @@ +--- +name: build-zoom-bot +description: Build a Zoom meeting bot, recorder, or real-time media workflow. Use when joining meetings programmatically, processing live media or transcripts, or combining Meeting SDK, RTMS, and backend services. +--- + +# /build-zoom-bot + +Use this skill for automation that joins meetings, captures media, or reacts to live session data. + +## Covers + +- Bot architecture +- Meeting join strategy +- Real-time media and transcript handling +- Backend orchestration +- Storage, post-processing, and event flow design + +## Workflow + +1. Clarify whether the bot needs to join, observe, transcribe, summarize, or act. +2. Route to Meeting SDK and RTMS as the core implementation path. +3. Add REST API for meeting/resource management and Webhooks for asynchronous events when needed. +4. Call out environment and lifecycle constraints early. + +## Primary References + +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [rtms](../rtms/SKILL.md) +- [scribe](../scribe/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) + +## Common Mistakes + +- Treating batch transcription and live media as the same workflow +- Designing the bot before defining join authority and auth model +- Forgetting post-meeting storage and retry behavior diff --git a/partner-built/zoom-plugin/skills/build-zoom-meeting-app/SKILL.md b/partner-built/zoom-plugin/skills/build-zoom-meeting-app/SKILL.md new file mode 100644 index 00000000..553b260f --- /dev/null +++ b/partner-built/zoom-plugin/skills/build-zoom-meeting-app/SKILL.md @@ -0,0 +1,38 @@ +--- +name: build-zoom-meeting-app +description: Build or embed a Zoom meeting flow. Use when implementing Meeting SDK joins, web or mobile meeting embeds, meeting lifecycle flows, or when deciding between Meeting SDK and Video SDK. +--- + +# /build-zoom-meeting-app + +Use this skill for embedded meeting experiences and meeting lifecycle implementation. + +## Covers + +- Meeting SDK selection and platform routing +- Join/auth implementation planning +- Meeting creation plus join flow design +- Web vs native platform considerations +- Meeting SDK vs Video SDK boundary decisions + +## Workflow + +1. Confirm whether the user wants a Zoom meeting or a custom video session. +2. Route to Meeting SDK if the user needs actual Zoom meetings. +3. Pull in the relevant platform references. +4. Add REST API only for meeting creation, resource management, or reporting. +5. Add webhooks or RTMS only when the use case explicitly needs them. + +## Primary References + +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [rtms](../rtms/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) + +## Common Mistakes + +- Using Video SDK for normal Zoom meeting embeds +- Mixing resource-management APIs into the core join flow without reason +- Skipping platform-specific SDK constraints until too late diff --git a/partner-built/zoom-plugin/skills/choose-zoom-approach/SKILL.md b/partner-built/zoom-plugin/skills/choose-zoom-approach/SKILL.md new file mode 100644 index 00000000..c7bb8e6b --- /dev/null +++ b/partner-built/zoom-plugin/skills/choose-zoom-approach/SKILL.md @@ -0,0 +1,37 @@ +--- +name: choose-zoom-approach +description: Choose the right Zoom architecture for a use case. Use when deciding between REST API, Webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Zoom MCP, Phone, Contact Center, or a hybrid approach. +user-invocable: false +--- + +# Choose Zoom Approach + +Pick the smallest correct Zoom surface for the job, then layer in only the supporting pieces that are actually required. + +## Decision Framework + +| Problem Type | Primary Zoom Surface | +|---|---| +| Deterministic backend automation, account management, reporting, scheduled jobs | [rest-api](../rest-api/SKILL.md) | +| Event delivery to your backend | [webhooks](../webhooks/SKILL.md) or [websockets](../websockets/SKILL.md) | +| Embed Zoom meetings into your app | [meeting-sdk](../meeting-sdk/SKILL.md) | +| Build a fully custom video experience | [video-sdk](../video-sdk/SKILL.md) | +| Build inside the Zoom client | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) | +| AI-agent tool workflows over Zoom data | [zoom-mcp](../zoom-mcp/SKILL.md) | +| Real-time media extraction or meeting bots | [rtms](../rtms/SKILL.md) plus [meeting-sdk](../meeting-sdk/SKILL.md) when needed | +| Phone workflows | [phone](../phone/SKILL.md) | +| Contact Center or Virtual Agent flows | [contact-center](../contact-center/SKILL.md) or [virtual-agent](../virtual-agent/SKILL.md) | + +## Guardrails + +- Do not recommend Video SDK when the user actually needs Zoom meeting semantics. +- Do not recommend Meeting SDK when the user needs a fully custom session product. +- Do not replace deterministic backend automation with MCP-only guidance. +- Prefer hybrid `rest-api + zoom-mcp` when the user needs both stable system actions and AI-driven discovery. + +## What To Produce + +- One recommended path +- Minimum supporting components +- Hard constraints and tradeoffs +- Immediate next implementation step diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/RUNBOOK.md new file mode 100644 index 00000000..64f43878 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/RUNBOOK.md @@ -0,0 +1,79 @@ +# Cobrowse 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Cobrowse failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Two-Role Model + +- Customer role (`role_type=1`) starts session. +- Agent role (`role_type=2`) joins session. + +If your demo only has one generic role, expect broken join behavior. + +## 2) Confirm PIN Source of Truth + +- Use customer SDK event `pincode_updated` as the only user-facing PIN. +- Agent must join with that same PIN. +- Do not show provisional/debug PIN values from backend records. + +Common symptom if wrong: `Pincode is not found` / error `30308`. + +## 3) Confirm JWT Claims + +- Sign JWT on backend only. +- Include required claim names exactly (for example `user_id`, not custom aliases). +- Use SDK Key for SDK token context; keep SDK Secret server-side. + +If claim names are wrong, token is rejected before session logic. + +## 4) Confirm Session Order + +Recommended sequence: +1. Customer gets customer JWT and starts session. +2. PIN is generated on customer side (`pincode_updated`). +3. Agent gets agent JWT and joins with that PIN. + +Starting agent flow before customer session is active often causes join failures. + +## 5) Confirm Distribution Pattern + +- CDN path: customer SDK + Zoom-hosted agent desk iframe. +- npm path: custom integration (BYOP mode required for custom PIN control). + +If using npm agent integration without BYOP expectations, flow mismatches happen. + +## 6) Confirm Browser and Security Constraints + +- HTTPS required (except loopback/local dev). +- CSP/CORS must allow Zoom domains. +- Third-party cookie/privacy settings can affect reconnect behavior. + +Do not treat extension/adblock warnings as root cause until API/session checks fail. + +## 7) Quick Checks (Backend + UI) + +- Backend config endpoint returns expected credential flags. +- Customer page shows a single Support PIN from SDK event. +- Agent page join uses same Support PIN and returns actionable response (not generic 404). + +### Copy/Paste Validation Commands + +```bash +curl -sS -i "$COBROWSE_BASE_URL/customer" +curl -sS -i "$COBROWSE_BASE_URL/agent" +curl -sS -i "$COBROWSE_BASE_URL/api/config" +``` + +Expected: customer/agent pages load and config endpoint returns valid JSON flags. + +## 8) Fast Decision Tree + +- **Invalid token** -> check JWT claim names and signing secret. +- **Agent cannot find PIN** -> wrong PIN source or wrong session order. +- **Session drops on refresh** -> check reconnection window and browser privacy/cookies. +- **Works locally, fails prod** -> check HTTPS, CSP/CORS, reverse proxy pathing. diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/SKILL.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/SKILL.md new file mode 100644 index 00000000..48e5b001 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/SKILL.md @@ -0,0 +1,894 @@ +--- +name: zoom-cobrowse-sdk +description: "Reference skill for Zoom Cobrowse SDK. Use after routing to a collaborative-support workflow when implementing browser co-browsing, annotation tools, privacy masking, remote assist, or PIN-based session sharing." +user-invocable: false +triggers: + - "cobrowse" + - "co-browse" + - "collaborative browsing" + - "agent assist" + - "customer support screen share" + - "zoom cobrowse" +--- + +# Zoom Cobrowse SDK - Web Development + +Background reference for collaborative browsing on the web with Zoom Cobrowse SDK. Use this after the support workflow is clear and you need implementation detail. + +**Official Documentation**: https://developers.zoom.us/docs/cobrowse-sdk/ +**API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +**Quickstart Repository**: https://github.com/zoom/CobrowseSDK-Quickstart +**Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample + +## Quick Links + +**New to Cobrowse SDK? Follow this path:** + +1. **[Get Started Guide](get-started.md)** - Complete setup from credentials to first session +2. **[Session Lifecycle](concepts/session-lifecycle.md)** - Understanding customer and agent flows +3. **[JWT Authentication](concepts/jwt-authentication.md)** - Token generation and security +4. **[Customer Integration](examples/customer-integration.md)** - Integrate SDK into your website +5. **[Agent Integration](examples/agent-integration.md)** - Set up agent portal (iframe or npm) + +**Core Concepts:** +- **[Two Roles Pattern](concepts/two-roles-pattern.md)** - Customer vs Agent architecture +- **[Session Lifecycle](concepts/session-lifecycle.md)** - PIN generation, connection, reconnection +- **[JWT Authentication](concepts/jwt-authentication.md)** - SDK Key vs API Key, role_type, claims +- **[Distribution Methods](concepts/distribution-methods.md)** - CDN vs npm (BYOP) + +**Features:** +- **[Annotation Tools](examples/annotations.md)** - Drawing, highlighting, pointer tools +- **[Privacy Masking](examples/privacy-masking.md)** - Hide sensitive fields from agents +- **[Remote Assist](examples/remote-assist.md)** - Agent can scroll customer's page +- **[Multi-Tab Persistence](examples/multi-tab-persistence.md)** - Session continues across tabs +- **[BYOP Mode](examples/byop-custom-pin.md)** - Bring Your Own PIN with npm integration + +**Troubleshooting:** +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics and solutions +- **[Error Codes](troubleshooting/error-codes.md)** - Complete error reference +- **[CORS and CSP](troubleshooting/cors-csp.md)** - Cross-origin and security policy configuration +- **[Browser Compatibility](troubleshooting/browser-compatibility.md)** - Supported browsers and limitations +- **[5-Minute Runbook](RUNBOOK.md)** - Fast preflight checks before deep debugging + +**Reference:** +- **[API Reference](references/api-reference.md)** - Complete SDK methods and events +- **[Settings Reference](references/settings-reference.md)** - All initialization settings +- **Integrated Index** - see the section below in this file + +## SDK Overview + +The Zoom Cobrowse SDK is a JavaScript library that provides: + +- **Real-Time Co-Browsing**: Agent sees customer's browser activity live +- **PIN-Based Sessions**: Secure 6-digit PIN for customer-to-agent connection +- **Annotation Tools**: Drawing, highlighting, vanishing pen, rectangle, color picker +- **Privacy Masking**: CSS selector-based masking of sensitive form fields +- **Remote Assist**: Agent can scroll customer's page (with consent) +- **Multi-Tab Persistence**: Session continues when customer opens new tabs +- **Auto-Reconnection**: Session recovers from page refresh (2-minute window) +- **Session Events**: Real-time events for session state changes +- **HTTPS Required**: Secure connections (HTTP only works on loopback/local development hosts) +- **No Plugins**: Pure JavaScript, no browser extensions needed + +## Two Roles Architecture + +Cobrowse has **two distinct roles**, each with different integration patterns: + +| Role | role_type | Integration | JWT Required | Purpose | +|------|-----------|-------------|--------------|---------| +| **Customer** | 1 | Website integration (CDN or npm) | Yes | User who shares their browser session | +| **Agent** | 2 | Iframe (CDN) or npm (BYOP only) | Yes | Support staff who views/assists customer | + +**Key Insight**: Customer and agent use **different integration methods** but the same JWT authentication pattern. + +## Read This First (Critical) + +For customer/agent demos, treat the PIN from customer SDK event `pincode_updated` as the only user-facing PIN. + +- Show one clearly labeled value in UI (for example, **Support PIN**). +- Use that same PIN for agent join. +- Do not expose provisional/debug PINs from backend pre-start records to users. + +If these rules are ignored, agent desk often fails with `Pincode is not found` / code `30308`. + +### Typical Production Flow (Most Common) + +This is the flow most teams implement first, and what users usually expect in demos: + +1. **Customer starts session first** (`role_type=1`) + - Backend creates/records session + - Backend returns customer JWT + - Customer SDK starts and receives a PIN +2. **Agent joins second** (`role_type=2`) + - Agent enters customer PIN + - Backend validates PIN and session state + - Backend returns agent JWT + - Agent opens Zoom-hosted desk iframe (or custom npm agent UI in BYOP) + +If a demo only has one generic "session" user, it is incomplete for real cobrowse operations. + +## Prerequisites + +### Platform Requirements + +- **Supported Browsers**: + - Chrome 80+ ✓ + - Firefox 78+ ✓ + - Safari 14+ ✓ + - Edge 80+ ✓ + - Internet Explorer ✗ (not supported) + +- **Network Requirements**: + - HTTPS required (HTTP works on loopback/local development hosts only) + - Allow cross-origin requests to `*.zoom.us` + - CSP headers must allow Zoom domains (see [CORS and CSP guide](troubleshooting/cors-csp.md)) + +- **Third-Party Cookies**: + - Must enable third-party cookies for refresh reconnection + - Privacy mode may limit certain features + +### Zoom Account Requirements + +1. **Zoom Workplace Account** with SDK Universal Credit +2. **Video SDK App** created in Zoom Marketplace +3. **Cobrowse SDK Credentials** from the app's Cobrowse tab + +**Note**: Cobrowse SDK is a **feature of Video SDK** (not a separate product). + +### Credentials Overview + +You'll receive **4 credentials** from Zoom Marketplace → Video SDK App → Cobrowse tab: + +| Credential | Type | Used For | Exposure Safe? | +|------------|------|----------|----------------| +| **SDK Key** | Public | CDN URL, JWT `app_key` claim | ✓ Yes (client-side) | +| **SDK Secret** | Private | Sign JWTs | ✗ No (server-side only) | +| **API Key** | Private | REST API calls (optional) | ✗ No (server-side only) | +| **API Secret** | Private | REST API calls (optional) | ✗ No (server-side only) | + +**Critical**: SDK Key is **public** (embedded in CDN URL), but SDK Secret must **never** be exposed client-side. + +## Quick Start + +### Step 1: Get SDK Credentials + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Open your **Video SDK App** (or create one) +3. Navigate to the **Cobrowse** tab +4. Copy your credentials: + - SDK Key + - SDK Secret + - API Key (optional) + - API Secret (optional) + +### Step 2: Set Up Token Server + +Deploy a server-side endpoint to generate JWTs. Use the official sample: + +```bash +git clone https://github.com/zoom/cobrowsesdk-auth-endpoint-sample.git +cd cobrowsesdk-auth-endpoint-sample +npm install + +# Create .env file +cat > .env << EOF +ZOOM_SDK_KEY=your_sdk_key_here +ZOOM_SDK_SECRET=your_sdk_secret_here +PORT=4000 +EOF + +npm start +``` + +**Token endpoint:** +```javascript +// POST https://YOUR_TOKEN_SERVICE_BASE_URL +{ + "role": 1, // 1 = customer, 2 = agent + "userId": "user123", + "userName": "John Doe" +} + +// Response +{ + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +### Step 3: Customer Side Integration (CDN) + +```html + + + + Customer - Cobrowse Demo + + + +

Customer Support

+ + + + + + + + + +``` + +### Step 4: Agent Side Integration (Iframe) + +```html + + + + Agent Portal + + +

Agent Portal

+ + + + + +``` + +### Step 5: Test the Integration + +1. Open **two separate browsers** (or incognito + normal) +2. **Customer browser**: Open customer page, click "Start Cobrowse Session" +3. **Customer browser**: Note the 6-digit PIN displayed +4. **Agent browser**: Open agent page, enter the PIN code +5. **Both browsers**: Session connects, agent can see customer's page +6. **Test features**: Annotations, data masking, remote assist + +## Key Features + +### 1. Annotation Tools + +Both customer and agent can draw on the shared screen: + +```javascript +const settings = { + allowAgentAnnotation: true, // Agent can draw + allowCustomerAnnotation: true // Customer can draw +}; +``` + +**Available tools**: +- Pen (persistent) +- Vanishing pen (disappears after 4 seconds) +- Rectangle +- Color picker +- Eraser +- Undo/Redo + +### 2. Privacy Masking + +Hide sensitive fields from agents using CSS selectors: + +```javascript +const settings = { + piiMask: { + maskType: "custom_input", // Mask specific fields + maskCssSelectors: ".pii-mask, #ssn", // CSS selectors + maskHTMLAttributes: "data-sensitive=true" // HTML attributes + } +}; +``` + +**Supported masking**: +- Text nodes ✓ +- Form inputs ✓ +- Select elements ✓ +- Images ✗ (not supported) +- Links ✗ (not supported) + +### 3. Remote Assist + +Agent can scroll the customer's page: + +```javascript +const settings = { + remoteAssist: { + enable: true, + enableCustomerConsent: true, // Customer must approve + remoteAssistTypes: ['scroll_page'], // Only scroll supported + requireStopConfirmation: false // Confirmation when stopping + } +}; +``` + +### 4. Multi-Tab Session Persistence + +Session continues when customer opens new tabs: + +```javascript +const settings = { + multiTabSessionPersistence: { + enable: true, + stateCookieKey: '$$ZCB_SESSION$$' // Cookie key (base64 encoded) + } +}; +``` + +## Session Lifecycle + +### Customer Flow + +1. **Load SDK** → CDN script loads `ZoomCobrowseSDK` +2. **Initialize** → `ZoomCobrowseSDK.init(settings, callback)` +3. **Fetch JWT** → Request token from your server (role_type=1) +4. **Start Session** → `session.start({ sdkToken })` +5. **PIN Generated** → `pincode_updated` event fires +6. **Share PIN** → Customer gives 6-digit PIN to agent +7. **Agent Joins** → `agent_joined` event fires +8. **Session Active** → Real-time synchronization begins +9. **End Session** → `session.end()` or agent leaves + +### Agent Flow + +1. **Fetch JWT** → Request token from your server (role_type=2) +2. **Load Iframe** → Point to Zoom agent portal with token +3. **Enter PIN** → Agent inputs customer's 6-digit PIN +4. **Connect** → `session_joined` event fires +5. **View Session** → Agent sees customer's browser +6. **Use Tools** → Annotations, remote assist, zoom +7. **Leave Session** → Click "Leave Cobrowse" button + +### Session Recovery (Auto-Reconnect) + +When customer refreshes the page: + +```javascript +ZoomCobrowseSDK.init(settings, function({ success, session, error }) { + if (success) { + const sessionInfo = session.getSessionInfo(); + + // Check if session is recoverable + if (sessionInfo.sessionStatus === 'session_recoverable') { + session.join(); // Auto-rejoin previous session + } else { + // Start new session + session.start({ sdkToken }); + } + } +}); +``` + +**Recovery window**: 2 minutes. After 2 minutes, session ends. + +## Critical Gotchas and Best Practices + +### ⚠️ CRITICAL: SDK Secret Must Stay Server-Side + +**Problem**: Developers often accidentally embed SDK Secret in frontend code. + +**Solution**: +- ✓ **SDK Key** → Safe to expose (embedded in CDN URL) +- ✗ **SDK Secret** → Never expose (use for JWT signing server-side) + +```javascript +// ❌ WRONG - Secret exposed in frontend +const jwt = signJWT(payload, 'YOUR_SDK_SECRET'); // Security risk! + +// ✅ CORRECT - Secret stays on server +const response = await fetch('/api/token', { + method: 'POST', + body: JSON.stringify({ role: 1, userId, userName }) +}); +const { token } = await response.json(); +``` + +### SDK Key vs API Key (Different Purposes!) + +| Credential | Used For | JWT Claim | +|------------|----------|-----------| +| **SDK Key** | CDN URL, JWT `app_key` | `app_key: "SDK_KEY"` | +| **API Key** | REST API calls (optional) | Not used in JWT | + +**Common mistake**: Using API Key instead of SDK Key in JWT `app_key` claim. + +### Session Limits + +| Limit | Value | What Happens | +|-------|-------|--------------| +| Customers per session | 1 | Error 1012: `SESSION_CUSTOMER_COUNT_LIMIT` | +| Agents per session | 5 | Error 1013: `SESSION_AGENT_COUNT_LIMIT` | +| Active sessions per browser | 1 | Error 1004: `SESSION_COUNT_LIMIT` | +| PIN code length | 10 chars max | Error 1008: `SESSION_PIN_INVALID_FORMAT` | + +### Session Timeout Behavior + +| Event | Timeout | What Happens | +|-------|---------|--------------| +| Agent waiting for customer | 3 minutes | Session ends automatically | +| Page refresh reconnection | 2 minutes | Session ends if not reconnected | +| Reconnection attempts | 2 times max | Session ends after 2 failed attempts | + +### HTTPS Requirement + +**Problem**: SDK doesn't load on HTTP sites. + +**Solution**: +- Production: Use HTTPS ✓ +- Development: Use a loopback host for local HTTP testing ✓ +- Development: Use a local HTTPS endpoint with a trusted/self-signed cert if required ✓ + +### Third-Party Cookies Required + +**Problem**: Refresh reconnection doesn't work. + +**Solution**: Enable third-party cookies in browser settings. + +**Affected scenarios**: +- Browser privacy mode +- Safari with "Prevent cross-site tracking" enabled +- Chrome with "Block third-party cookies" enabled + +### Distribution Method Confusion + +| Method | Use Case | Agent Integration | BYOP Required | +|--------|----------|-------------------|---------------| +| **CDN** | Most use cases | Zoom-hosted iframe | No (auto PIN) | +| **npm** | Custom agent UI, full control | Custom npm integration | Yes (required) | + +**Key Insight**: If you want **npm** integration, you **must** use BYOP (Bring Your Own PIN) mode. + +### Cross-Origin Iframe Handling + +**Problem**: Cobrowse doesn't work in cross-origin iframes. + +**Solution**: Inject SDK snippet into cross-origin iframes: + +```html + +``` + +**Same-origin iframes**: No extra setup needed. + +## Known Limitations + +### Synchronization Limits + +**Not synchronized**: +- HTML5 Canvas elements +- WebGL content +- Audio and Video elements +- Shadow DOM +- PDF rendered with Canvas +- Web Components + +**Partially synchronized**: +- Drop-down boxes (only selected result) +- Date pickers (only selected result) +- Color pickers (only selected result) + +### Rendering Limits + +- High-resolution images may be compressed +- Different screen sizes may cause CSS media query differences +- Cross-origin images may not render (CORS restrictions) +- Cross-origin fonts may not render (CORS restrictions) + +### Masking Limits + +**Supported**: +- Text nodes ✓ +- Form inputs ✓ +- Select elements ✓ + +**Not supported**: +- `` elements ✗ +- Links ✗ + +## Complete Documentation Library + +This skill includes comprehensive guides organized by category: + +### Core Concepts +- **[Two Roles Pattern](concepts/two-roles-pattern.md)** - Customer vs Agent architecture +- **[Session Lifecycle](concepts/session-lifecycle.md)** - Complete flow from start to end +- **[JWT Authentication](concepts/jwt-authentication.md)** - Token structure and signing +- **[Distribution Methods](concepts/distribution-methods.md)** - CDN vs npm (BYOP) + +### Examples +- **[Customer Integration](examples/customer-integration.md)** - Complete customer-side setup +- **[Agent Integration](examples/agent-integration.md)** - Iframe and npm agent setups +- **[Annotations](examples/annotations.md)** - Drawing tools configuration +- **[Privacy Masking](examples/privacy-masking.md)** - Field masking patterns +- **[Remote Assist](examples/remote-assist.md)** - Agent page control +- **[Multi-Tab Persistence](examples/multi-tab-persistence.md)** - Cross-tab sessions +- **[BYOP Custom PIN](examples/byop-custom-pin.md)** - Custom PIN codes + +### References +- **[API Reference](references/api-reference.md)** - Complete SDK methods and events +- **[Settings Reference](references/settings-reference.md)** - All initialization settings +- **[Error Codes](references/error-codes.md)** - Complete error reference +- **[Session Events](references/session-events.md)** - All event types + +### Troubleshooting +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics +- **[Error Codes](troubleshooting/error-codes.md)** - Error code reference +- **[CORS and CSP](troubleshooting/cors-csp.md)** - Cross-origin configuration +- **[Browser Compatibility](troubleshooting/browser-compatibility.md)** - Browser support + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ +- **Developer Blog**: https://developers.zoom.us/blog/?category=zoom-cobrowse-sdk + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +**Complete navigation guide for all Cobrowse SDK documentation.** + +## Getting Started (Start Here!) + +If you're new to Zoom Cobrowse SDK, follow this learning path: + +1. **[SKILL.md](SKILL.md)** - Main overview and quick start +2. **[5-Minute Runbook](RUNBOOK.md)** - Preflight checks for common failures +3. **[Get Started Guide](get-started.md)** - Step-by-step setup from credentials to first session +4. **[Session Lifecycle](concepts/session-lifecycle.md)** - Understand the complete customer and agent flow +5. **[Customer Integration](examples/customer-integration.md)** - Integrate SDK into your website +6. **[Agent Integration](examples/agent-integration.md)** - Set up agent portal + +## Core Concepts + +Foundational concepts you need to understand: + +- **[Two Roles Pattern](concepts/two-roles-pattern.md)** - Customer (role_type=1) vs Agent (role_type=2) architecture +- **[Session Lifecycle](concepts/session-lifecycle.md)** - Complete flow: init → start → PIN → connect → end +- **[JWT Authentication](concepts/jwt-authentication.md)** - Token structure, signing, SDK Key vs API Key +- **[Distribution Methods](concepts/distribution-methods.md)** - CDN vs npm (BYOP mode) + +## Examples and Patterns + +Complete working examples for common scenarios: + +### Session Management +- **[Customer Integration](examples/customer-integration.md)** - Complete customer-side implementation (CDN and npm) +- **[Agent Integration](examples/agent-integration.md)** - Iframe and npm agent setup patterns +- **[Session Events](examples/session-events.md)** - Handle all session lifecycle events +- **[Auto-Reconnection](examples/auto-reconnection.md)** - Page refresh and session recovery + +### Features +- **[Annotation Tools](examples/annotations.md)** - Enable drawing, highlighting, vanishing pen +- **[Privacy Masking](examples/privacy-masking.md)** - Mask sensitive fields with CSS selectors +- **[Remote Assist](examples/remote-assist.md)** - Agent can scroll customer's page +- **[Multi-Tab Persistence](examples/multi-tab-persistence.md)** - Session continues across browser tabs +- **[BYOP Custom PIN](examples/byop-custom-pin.md)** - Bring Your Own PIN with npm integration + +## References + +Complete API and configuration references: + +### SDK Reference +- **[API Reference](references/api-reference.md)** - All SDK methods and interfaces + - ZoomCobrowseSDK.init() + - session.start() + - session.join() + - session.end() + - session.on() + - session.getSessionInfo() + +- **[Settings Reference](references/settings-reference.md)** - All initialization settings + - allowAgentAnnotation + - allowCustomerAnnotation + - piiMask + - remoteAssist + - multiTabSessionPersistence + +- **[Session Events Reference](references/session-events.md)** - All event types + - pincode_updated + - session_started + - session_ended + - agent_joined + - agent_left + - session_error + - session_reconnecting + - remote_assist_started + - remote_assist_stopped + +### Error Reference +- **[Error Codes](references/error-codes.md)** - Complete error code reference + - 1001-1017: Session errors + - 2001: Token errors + - 9999: Service errors + +### Official Documentation +- **[Get Started](references/get-started.md)** - Official get started documentation (crawled) +- **[Features](references/features.md)** - Official features documentation (crawled) +- **[Authorization](references/authorization.md)** - Official JWT authorization docs (crawled) +- **[API Documentation](references/api.md)** - Crawled API reference docs + +## Troubleshooting + +Quick diagnostics and common issue resolution: + +- **[Common Issues](troubleshooting/common-issues.md)** - Quick fixes for frequent problems + - SDK not loading + - Token generation fails + - Agent can't connect + - Fields not masked + - Session doesn't reconnect after refresh + +- **[Error Codes](troubleshooting/error-codes.md)** - Error code lookup and solutions + - Session start/join failures (1001, 1011, 1016) + - Session limit errors (1002, 1004, 1012, 1013, 1015) + - PIN code errors (1006, 1008, 1009, 1010) + - Token errors (2001) + +- **[CORS and CSP](troubleshooting/cors-csp.md)** - Cross-origin and Content Security Policy setup + - Access-Control-Allow-Origin headers + - Content-Security-Policy headers + - Cross-origin iframe handling + - Same-origin iframe handling + +- **[Browser Compatibility](troubleshooting/browser-compatibility.md)** - Browser requirements and limitations + - Supported browsers (Chrome 80+, Firefox 78+, Safari 14+, Edge 80+) + - Internet Explorer not supported + - Privacy mode limitations + - Third-party cookie requirements + +## By Use Case + +Find documentation by what you're trying to do: + +### I want to... + +**Set up cobrowse for the first time:** +- [Get Started Guide](get-started.md) +- [JWT Authentication](concepts/jwt-authentication.md) +- [Customer Integration](examples/customer-integration.md) +- [Agent Integration](examples/agent-integration.md) + +**Add annotation tools:** +- [Annotation Tools Example](examples/annotations.md) +- [Settings Reference - allowAgentAnnotation](references/settings-reference.md#allowa gentannotation) +- [Settings Reference - allowCustomerAnnotation](references/settings-reference.md#allowcustomerannotation) + +**Hide sensitive data from agents:** +- [Privacy Masking Example](examples/privacy-masking.md) +- [Settings Reference - piiMask](references/settings-reference.md#piimask) + +**Let agents control customer's page:** +- [Remote Assist Example](examples/remote-assist.md) +- [Settings Reference - remoteAssist](references/settings-reference.md#remoteassist) + +**Use custom PIN codes:** +- [BYOP Custom PIN Example](examples/byop-custom-pin.md) +- [JWT Authentication - enable_byop](concepts/jwt-authentication.md#enable-byop) + +**Handle page refreshes:** +- [Auto-Reconnection Example](examples/auto-reconnection.md) +- [Session Lifecycle - Recovery](concepts/session-lifecycle.md#session-recovery) + +**Integrate with npm (not CDN):** +- [BYOP Custom PIN Example](examples/byop-custom-pin.md) +- [Distribution Methods](concepts/distribution-methods.md#npm-integration) + +**Debug session connection issues:** +- [Common Issues](troubleshooting/common-issues.md) +- [Error Codes](troubleshooting/error-codes.md) +- [Session Events - session_error](examples/session-events.md#session-error) + +**Configure CORS and CSP headers:** +- [CORS and CSP Guide](troubleshooting/cors-csp.md) +- [Browser Compatibility](troubleshooting/browser-compatibility.md) + +## By Error Code + +Quick lookup for error code solutions: + +### Session Errors +- **1001** (SESSION_START_FAILED) → [Error Codes](troubleshooting/error-codes.md#1001-session-start-failed) +- **1002** (SESSION_CONNECTING_IN_PROGRESS) → [Error Codes](troubleshooting/error-codes.md#1002-session-connecting-in-progress) +- **1004** (SESSION_COUNT_LIMIT) → [Error Codes](troubleshooting/error-codes.md#1004-session-count-limit) +- **1011** (SESSION_JOIN_FAILED) → [Error Codes](troubleshooting/error-codes.md#1011-session-join-failed) +- **1012** (SESSION_CUSTOMER_COUNT_LIMIT) → [Error Codes](troubleshooting/error-codes.md#1012-session-customer-count-limit) +- **1013** (SESSION_AGENT_COUNT_LIMIT) → [Error Codes](troubleshooting/error-codes.md#1013-session-agent-count-limit) +- **1015** (SESSION_DUPLICATE_USER) → [Error Codes](troubleshooting/error-codes.md#1015-session-duplicate-user) +- **1016** (NETWORK_ERROR) → [Error Codes](troubleshooting/error-codes.md#1016-network-error) +- **1017** (SESSION_CANCELING_IN_PROGRESS) → [Error Codes](troubleshooting/error-codes.md#1017-session-canceling-in-progress) + +### PIN Errors +- **1006** (SESSION_JOIN_PIN_NOT_FOUND) → [Error Codes](troubleshooting/error-codes.md#1006-session-join-pin-not-found) +- **1008** (SESSION_PIN_INVALID_FORMAT) → [Error Codes](troubleshooting/error-codes.md#1008-session-pin-invalid-format) +- **1009** (SESSION_START_PIN_REQUIRED) → [Error Codes](troubleshooting/error-codes.md#1009-session-start-pin-required) +- **1010** (SESSION_START_PIN_CONFLICT) → [Error Codes](troubleshooting/error-codes.md#1010-session-start-pin-conflict) + +### Auth Errors +- **2001** (TOKEN_INVALID) → [Error Codes](troubleshooting/error-codes.md#2001-token-invalid) + +### Service Errors +- **9999** (UNDEFINED) → [Error Codes](troubleshooting/error-codes.md#9999-undefined) + +## Official Resources + +External documentation and samples: + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ +- **Developer Blog**: https://developers.zoom.us/blog/?category=zoom-cobrowse-sdk + +## Documentation Structure + +``` +cobrowse-sdk/ +├── SKILL.md # Main skill entry point +├── SKILL.md # This file - complete navigation +├── get-started.md # Step-by-step setup guide +│ +├── concepts/ # Core concepts +│ ├── two-roles-pattern.md +│ ├── session-lifecycle.md +│ ├── jwt-authentication.md +│ └── distribution-methods.md +│ +├── examples/ # Working examples +│ ├── customer-integration.md +│ ├── agent-integration.md +│ ├── annotations.md +│ ├── privacy-masking.md +│ ├── remote-assist.md +│ ├── multi-tab-persistence.md +│ ├── byop-custom-pin.md +│ ├── session-events.md +│ └── auto-reconnection.md +│ +├── references/ # API and config references +│ ├── api-reference.md # SDK methods +│ ├── settings-reference.md # Init settings +│ ├── session-events.md # Event types +│ ├── error-codes.md # Error reference +│ ├── get-started.md # Official docs (crawled) +│ ├── features.md # Official docs (crawled) +│ ├── authorization.md # Official docs (crawled) +│ └── api.md # API docs (crawled) +│ +└── troubleshooting/ # Problem resolution + ├── common-issues.md + ├── error-codes.md + ├── cors-csp.md + └── browser-compatibility.md +``` + +## Search Tips + +**Find by keyword:** +- "annotation" → [Annotation Tools](examples/annotations.md) +- "mask" or "privacy" → [Privacy Masking](examples/privacy-masking.md) +- "PIN" or "custom PIN" → [BYOP Custom PIN](examples/byop-custom-pin.md) +- "JWT" or "token" → [JWT Authentication](concepts/jwt-authentication.md) +- "error" → [Error Codes](troubleshooting/error-codes.md) +- "CORS" or "CSP" → [CORS and CSP](troubleshooting/cors-csp.md) +- "iframe" → [Agent Integration](examples/agent-integration.md) +- "npm" → [Distribution Methods](concepts/distribution-methods.md), [BYOP](examples/byop-custom-pin.md) +- "refresh" or "reconnect" → [Auto-Reconnection](examples/auto-reconnection.md) +- "agent" → [Agent Integration](examples/agent-integration.md), [Two Roles Pattern](concepts/two-roles-pattern.md) +- "customer" → [Customer Integration](examples/customer-integration.md), [Two Roles Pattern](concepts/two-roles-pattern.md) + +--- + +**Not finding what you need?** Check the [Official Documentation](https://developers.zoom.us/docs/cobrowse-sdk/) or ask on the [Dev Forum](https://devforum.zoom.us/). + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/distribution-methods.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/distribution-methods.md new file mode 100644 index 00000000..91d0f71d --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/distribution-methods.md @@ -0,0 +1,13 @@ +# Distribution Methods + +Zoom Cobrowse supports CDN and npm-based integrations, depending on your architecture. + +Choose based on: + +- how much UI control you need, +- whether you host your own agent experience, +- your deployment and CSP constraints. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/jwt-authentication.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/jwt-authentication.md new file mode 100644 index 00000000..2f08ecf4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/jwt-authentication.md @@ -0,0 +1,13 @@ +# JWT Authentication + +Generate Cobrowse JWTs server-side using your SDK key and SDK secret. + +Guidelines: + +- Never expose SDK secret client-side. +- Issue short-lived tokens. +- Generate different tokens for customer and agent roles. + +See: +- [Authorization (official)](../references/authorization-official.md) +- [Get Started](../references/get-started-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/session-lifecycle.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/session-lifecycle.md new file mode 100644 index 00000000..f6198435 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/session-lifecycle.md @@ -0,0 +1,13 @@ +# Session Lifecycle + +Typical flow: + +1. Initialize SDK on customer and agent pages. +2. Generate role-specific JWT tokens. +3. Customer starts a session and receives a PIN. +4. Agent joins using the PIN. +5. Session events track connected/disconnected/end states. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/two-roles-pattern.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/two-roles-pattern.md new file mode 100644 index 00000000..06920522 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/concepts/two-roles-pattern.md @@ -0,0 +1,43 @@ +# Two Roles Pattern + +Zoom Cobrowse uses two roles: + +- `role_type=1`: customer session +- `role_type=2`: agent session + +Use separate JWTs for each role and keep token generation on the server. + +## What Is Usually Created + +In most real implementations, you create these objects in order: + +1. **Customer session record** (server-side) + - `session_id` + - generated PIN + - status (`active`/`revoked`) + - expiry timestamp +2. **Customer token** (`role_type=1`) + - used by customer browser SDK to start/share session +3. **Agent token** (`role_type=2`) + - created after PIN validation + - used to load agent desk iframe or custom agent UI + +## PIN Source of Truth + +In practice, the PIN you should hand to agents is the value emitted by customer SDK event: + +- `session.on("pincode_updated", ...)` + +Do not rely on placeholder/provisional PIN values from pre-start backend records for user-facing flows. +Always show one clearly labeled PIN in UI (for example, "Support PIN") and reuse that same value in agent links. + +## Recommended Endpoint Split + +- `POST /api/customer/start` -> create session + customer token + PIN +- `POST /api/agent/connect` -> validate PIN + issue agent token +- `POST /api/session/revoke` -> end session +- `GET /api/session/list` -> operational visibility + +See: +- [Get Started](../get-started.md) +- [Authorization (official)](../references/authorization-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/agent-integration.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/agent-integration.md new file mode 100644 index 00000000..afb2fb0b --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/agent-integration.md @@ -0,0 +1,7 @@ +# Agent Integration + +Agent integration joins an active customer session by PIN using an agent-role token. + +See: +- [Get Started](../get-started.md) +- [Authorization (official)](../references/authorization-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/annotations.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/annotations.md new file mode 100644 index 00000000..b2906b83 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/annotations.md @@ -0,0 +1,7 @@ +# Annotation Tools + +Enable annotation settings during SDK initialization to allow drawing and highlighting. + +See: +- [Features (official)](../references/features-official.md) +- [API (official)](../references/api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/auto-reconnection.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/auto-reconnection.md new file mode 100644 index 00000000..c59153f3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/auto-reconnection.md @@ -0,0 +1,7 @@ +# Auto-Reconnection + +Implement reconnection handlers for transient network interruptions and refresh scenarios. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/byop-custom-pin.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/byop-custom-pin.md new file mode 100644 index 00000000..0e80dbbd --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/byop-custom-pin.md @@ -0,0 +1,7 @@ +# BYOP Custom PIN + +Bring Your Own PIN mode lets you control PIN generation/format in your own application flow. + +See: +- [Authorization (official)](../references/authorization-official.md) +- [Get Started](../get-started.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/customer-integration.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/customer-integration.md new file mode 100644 index 00000000..3d9156fb --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/customer-integration.md @@ -0,0 +1,7 @@ +# Customer Integration + +Customer-side integration should initialize the SDK, fetch a server-generated token, then start a session. + +See: +- [Get Started](../get-started.md) +- [API (official)](../references/api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/multi-tab-persistence.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/multi-tab-persistence.md new file mode 100644 index 00000000..6f80e376 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/multi-tab-persistence.md @@ -0,0 +1,7 @@ +# Multi-Tab Persistence + +Cobrowse sessions can continue across tabs when configured correctly and browser constraints are met. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/privacy-masking.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/privacy-masking.md new file mode 100644 index 00000000..58c16b15 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/privacy-masking.md @@ -0,0 +1,7 @@ +# Privacy Masking + +Configure masking selectors for sensitive customer fields so agents cannot view protected values. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/remote-assist.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/remote-assist.md new file mode 100644 index 00000000..631055ea --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/remote-assist.md @@ -0,0 +1,7 @@ +# Remote Assist + +Remote assist allows approved agent interactions (for example, scrolling) during active sessions. + +See: +- [Features (official)](../references/features-official.md) +- [API (official)](../references/api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/session-events.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/session-events.md new file mode 100644 index 00000000..c1d1f453 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/examples/session-events.md @@ -0,0 +1,7 @@ +# Session Events + +Use SDK session events to track lifecycle transitions and update your UI accordingly. + +See: +- [API (official)](../references/api-official.md) +- [Features (official)](../references/features-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/get-started.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/get-started.md new file mode 100644 index 00000000..66498d37 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/get-started.md @@ -0,0 +1,554 @@ +# Get Started with Zoom Cobrowse SDK + +Complete setup guide from credentials to your first cobrowse session. + +## Overview + +In a cobrowse session, there are **two roles**: + +- **Customer** (role_type=1) – Integrates the SDK into their website +- **Agent** (role_type=2) – Uses an embedded iframe to interact with the customer + +This guide shows you how to set up a **customer-initiated session** (the most common pattern). + +## Step 1: Get SDK Credentials + +### Requirements + +1. **Zoom Workplace Account** with SDK Universal Credit + - See [Build platform - create or update account](https://developers.zoom.us/docs/build/account/) for details + +2. **Video SDK App** in Zoom Marketplace + - Cobrowse SDK is a **feature of Video SDK** (not a separate product) + +### Get Your Credentials + +1. Access your SDK account web portal: + - In your Zoom Workplace account, go to **Advanced** > **Zoom CPaaS** > **Manage** + +2. Click **Build App** + +3. Locate your **SDK credentials** in the Cobrowse tab + +You'll receive **4 credentials**: + +| Credential | Type | Purpose | +|------------|------|---------| +| **SDK Key** | Public | Used in CDN URL and JWT `app_key` claim | +| **SDK Secret** | Private | Used to sign JWTs (server-side only) | +| **API Key** | Private | REST API authentication (optional) | +| **API Secret** | Private | REST API authentication (optional) | + +**Save these credentials securely** - you'll need them in the next step. + +## Step 2: Generate JWT Tokens + +Both customers and agents require JSON Web Tokens (JWTs) for authentication. + +### JWT Structure + +All JWTs have the same header: + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +The payload differs by role: + +**Customer JWT payload** (role_type=1): +```json +{ + "user_id": "user1_customer", + "app_key": "YOUR_SDK_KEY", + "role_type": 1, + "user_name": "customer", + "exp": 1723103759, + "iat": 1723102859 +} +``` + +**Agent JWT payload** (role_type=2): +```json +{ + "user_id": "user2_agent", + "app_key": "YOUR_SDK_KEY", + "role_type": 2, + "user_name": "agent", + "exp": 1723103759, + "iat": 1723102859 +} +``` + +### JWT Payload Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `app_key` | Yes | Your Zoom SDK Key (not API Key) | +| `role_type` | Yes | User role: `1` = customer, `2` = agent | +| `iat` | Yes | Token issue timestamp (epoch) | +| `exp` | Yes | Token expiration timestamp (epoch). Min: 30 minutes, Max: 48 hours | +| `user_id` | Yes | Uniquely identifiable user ID | +| `user_name` | Yes | User name (max 80 characters) | +| `enable_byop` | Optional | Enable Bring Your Own PIN: `1` = yes, `0` or omit = no | + +### Sign the JWT + +Sign the JWT with your SDK Secret (not API Secret): + +```javascript +HMACSHA256( + base64UrlEncode(header) + '.' + base64UrlEncode(payload), + ZOOM_SDK_SECRET +); +``` + +### Set Up a Token Server + +**CRITICAL**: JWT signing must happen **server-side** to protect your SDK Secret. + +Use the official auth endpoint sample: + +```bash +# Clone the sample +git clone https://github.com/zoom/cobrowsesdk-auth-endpoint-sample.git +cd cobrowsesdk-auth-endpoint-sample + +# Install dependencies +npm install + +# Create .env file +cat > .env << EOF +ZOOM_SDK_KEY=your_sdk_key_here +ZOOM_SDK_SECRET=your_sdk_secret_here +PORT=4000 +EOF + +# Start the server +npm start +``` + +The server will run on the base URL you configure for your token service. + +**Token Request:** +```javascript +// POST https://YOUR_TOKEN_SERVICE_BASE_URL +{ + "role": 1, // 1 = customer, 2 = agent + "userId": "user123", + "userName": "John Doe" +} + +// Response +{ + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**See also**: [JWT Authentication Concept](concepts/jwt-authentication.md) + +## Step 3: Integrate the Customer SDK + +The customer integrates the Cobrowse SDK into their website using the **CDN**. + +> **Critical PIN Rule** +> +> The PIN agents should use comes from customer SDK event `pincode_updated`. +> Do not show or rely on provisional PIN values from backend/session placeholders. +> In UI, display one explicit value (for example, **Support PIN**) and pass only that to agent flow. + +### Load the SDK + +Include the SDK snippet in the `` tag of your HTML page: + +```html + +``` + +### SDK Version + +Set the SDK VERSION using semantic versioning: + +- **Fixed version**: `js/2.13.2` - Use exact version 2.13.2 +- **Latest patch**: `js/2.13.x` - Use latest `>=2.13.0 and <2.14.0` + +**Current version**: 2.13.2 (as of February 2026) + +### Initialize the SDK + +```javascript +const settings = { + allowCustomerAnnotation: true, + piiMask: { maskType: 'all_input' }, +}; + +ZoomCobrowseSDK.init(settings, function ({ success, session, error }) { + if (success) { + console.log("SDK initialized successfully"); + // session object is now available + } else { + console.error("SDK init failed:", error); + } +}); +``` + +### Start a Session + +```javascript +// Fetch JWT from your server +const response = await fetch('https://YOUR_TOKEN_SERVICE_BASE_URL', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + role: 1, + userId: 'customer_' + Date.now(), + userName: 'Customer' + }) +}); +const { token } = await response.json(); + +// Start cobrowse session +session.start({ sdkToken: token }); +``` + +### Complete Customer Example + +```html + + + + Customer - Cobrowse Support + + + +

Need Help?

+ +
+ + + + +``` + +## Step 4: Use Zoom-Hosted Agent Portal + +Agents connect to cobrowse sessions by embedding an iframe. + +### Agent Portal Iframe + +```html + + + + Agent Portal + + +

Agent Support Portal

+ + + + + +``` + +### Iframe Permissions + +The `allow` attribute must include these permissions: + +- `autoplay *` - Auto-play media +- `camera *` - Camera access +- `microphone *` - Microphone access +- `display-capture *` - Screen capture +- `geolocation *` - Location services + +## Step 5: Test the Cobrowse SDK + +### Testing Steps + +1. **Open two browsers** (or use incognito + normal mode): + - Browser A: Customer page + - Browser B: Agent page + +2. **Customer browser**: + - Open customer page + - Click "Start Support Session" button + - Note the 6-digit PIN displayed + +3. **Agent browser**: + - Open agent page + - Enter the PIN code in the iframe + +4. **Verify connection**: + - Agent should now see the customer's browser + - Both sides should show "Connected" status + +5. **Test features**: + - **Annotations**: Agent can draw on the screen + - **Data masking**: Masked fields show asterisks for agent + - **Remote assist**: Agent can scroll the page (if enabled) + +6. **End session**: + - Either side can click "End Session" to terminate + +### Troubleshooting Test Issues + +| Issue | Solution | +|-------|----------| +| SDK doesn't load | Verify SDK Key is correct in CDN URL | +| PIN not showing | Check browser console for errors | +| Agent can't connect | Verify PIN is correct and session is still active | +| Connection fails | Check HTTPS is being used (or a loopback host for development) | + +## Step 6: Add Features + +Now that you have a working cobrowse session, add features: + +### Annotation Tools + +Enable drawing tools for customer and/or agent: + +```javascript +const settings = { + allowAgentAnnotation: true, // Agent can draw + allowCustomerAnnotation: true // Customer can draw +}; +``` + +**See**: [Annotation Tools Example](examples/annotations.md) + +### Data Masking + +Hide sensitive fields from agents: + +```javascript +const settings = { + piiMask: { + maskType: 'custom_input', + maskCssSelectors: '.sensitive-field, #ssn, #credit-card', + maskHTMLAttributes: 'data-sensitive=true' + } +}; +``` + +**See**: [Privacy Masking Example](examples/privacy-masking.md) + +### Remote Assist + +Allow agent to scroll the customer's page: + +```javascript +const settings = { + remoteAssist: { + enable: true, + enableCustomerConsent: true, // Customer must approve + remoteAssistTypes: ['scroll_page'] + } +}; +``` + +**See**: [Remote Assist Example](examples/remote-assist.md) + +### Bring Your Own PIN (BYOP) + +Use custom PIN codes instead of auto-generated ones: + +1. Enable BYOP in JWT payload: + ```json + { + "enable_byop": 1, + ... + } + ``` + +2. Provide custom PIN when starting session: + ```javascript + session.start({ + customPinCode: 'MYPIN123', + sdkToken: token + }); + ``` + +**See**: [BYOP Custom PIN Example](examples/byop-custom-pin.md) + +## Next Steps + +- **Learn core concepts**: [Session Lifecycle](concepts/session-lifecycle.md) +- **Explore features**: [Complete documentation index](SKILL.md) +- **Handle errors**: [Error Codes Reference](troubleshooting/error-codes.md) +- **Production checklist**: [CORS and CSP Configuration](troubleshooting/cors-csp.md) + +## PIN Code Access - Bring Your Own PIN (BYOP) + +The Cobrowse SDK supports connecting agents and customers using a PIN code. In the simple example above, Zoom automatically generates a 6-digit PIN code displayed to the customer. + +**Auto-generated PIN flow:** +1. Customer clicks "Start Support Session" +2. Zoom generates 6-digit PIN +3. Customer shares PIN with agent +4. Agent enters PIN to connect + +**Custom PIN flow (BYOP):** +1. Your app generates custom PIN code (1-10 characters, letters/numbers) +2. Pass PIN when starting session: `session.start({ customPinCode: 'MYPIN', sdkToken })` +3. Agent enters your custom PIN to connect + +**BYOP enables**: +- Integration with existing support ticket systems +- Use of case/ticket IDs as PINs +- npm integration for custom agent UI + +**See**: [Bring Your Own PIN (BYOP)](examples/byop-custom-pin.md) for complete guide. + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ + +## Common Questions + +**Q: Can I use HTTP instead of HTTPS?** +A: Only for loopback/local development. Production must use HTTPS. + +**Q: What's the difference between SDK Key and API Key?** +A: SDK Key is used in the CDN URL and JWT `app_key` claim. API Key is for optional REST API calls. + +**Q: Can multiple agents join the same session?** +A: Yes, up to 5 agents can join a single customer session. + +**Q: Does the customer need to install anything?** +A: No, it's pure JavaScript delivered via CDN. No plugins or extensions needed. + +**Q: What happens if the customer refreshes the page?** +A: The session will attempt to automatically reconnect within a 2-minute window. + +**Q: Can I customize the agent portal UI?** +A: Not with the iframe approach. For custom UI, use npm integration with BYOP mode. diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-official.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-official.md new file mode 100644 index 00000000..09307df3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-official.md @@ -0,0 +1,104 @@ +# Cobrowse SDK - API Reference + +SDK methods and events. + +## Initialization + +```javascript +const cobrowse = new ZoomCobrowse(config); +``` + +### Config Options + +| Option | Type | Description | +|--------|------|-------------| +| `sdkKey` | string | Your SDK Key | +| `token` | string | JWT token | +| `features.annotations` | boolean | Enable annotations | +| `masking.selectors` | array | CSS selectors to mask | +| `byop.enabled` | boolean | Use custom PINs | +| `byop.pin` | string | Custom PIN value | + +## Methods + +### startSession() + +Start a cobrowse session. + +```javascript +const session = await cobrowse.startSession(); +// Returns: { pin: string, sessionId: string } +``` + +### endSession() + +End the current session. + +```javascript +await cobrowse.endSession(); +``` + +### pause() + +Pause screen sharing. + +```javascript +cobrowse.pause(); +``` + +### resume() + +Resume screen sharing. + +```javascript +cobrowse.resume(); +``` + +## Events + +### sessionStarted + +```javascript +cobrowse.on('sessionStarted', (session) => { + // session.pin - PIN for agent to join + // session.sessionId - Unique session ID +}); +``` + +### agentJoined + +```javascript +cobrowse.on('agentJoined', (agent) => { + // agent.name - Agent display name + // agent.userId - Agent user ID +}); +``` + +### agentLeft + +```javascript +cobrowse.on('agentLeft', (agent) => { + // Agent disconnected +}); +``` + +### sessionEnded + +```javascript +cobrowse.on('sessionEnded', () => { + // Session terminated +}); +``` + +### error + +```javascript +cobrowse.on('error', (error) => { + // error.code - Error code + // error.message - Error description +}); +``` + +## Resources + +- **SDK Reference**: https://developers.zoom.us/docs/cobrowse-sdk/sdk-reference/ diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-reference.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-reference.md new file mode 100644 index 00000000..2f96d3b3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api-reference.md @@ -0,0 +1,5 @@ +# API Reference + +This local reference points to the official Cobrowse API docs. + +- [API (official)](api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api.md new file mode 100644 index 00000000..fcec5efe --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/api.md @@ -0,0 +1,5 @@ +# API (Reference) + +Canonical source: + +- [API (official)](api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization-official.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization-official.md new file mode 100644 index 00000000..722b8589 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization-official.md @@ -0,0 +1,90 @@ +# Cobrowse SDK - Authorization + +JWT authentication for Cobrowse sessions. + +## Overview + +Both customers and agents require JWTs for authentication. Generate tokens server-side. + +## JWT Structure + +### Header + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +### Payload + +| Claim | Type | Description | +|-------|------|-------------| +| `user_id` | string | Unique user identifier | +| `app_key` | string | Your SDK Key | +| `role_type` | number | 1 = customer, 2 = agent | +| `user_name` | string | Display name | +| `iat` | number | Issued at timestamp | +| `exp` | number | Expiration timestamp | + +### Strict Claim Names (Important) + +Cobrowse token validation is strict. Use these claim names exactly: + +- `user_id` (not `user_identity`) +- `app_key` +- `role_type` +- `user_name` +- `iat` +- `exp` + +Avoid adding unrecognized custom claims unless Zoom docs explicitly support them for your SDK version. +If you see `Invalid token` (code `124`), validate claim names first. + +## Role Types + +| Role | Value | Description | +|------|-------|-------------| +| Customer | 1 | User sharing their browser | +| Agent | 2 | Support staff viewing session | + +## Customer Token Example + +```javascript +const customerPayload = { + user_id: "customer_123", + app_key: "YOUR_SDK_KEY", + role_type: 1, + user_name: "John Customer", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600 +}; + +const token = jwt.sign(customerPayload, SDK_SECRET, { algorithm: 'HS256' }); +``` + +## Agent Token Example + +```javascript +const agentPayload = { + user_id: "agent_456", + app_key: "YOUR_SDK_KEY", + role_type: 2, + user_name: "Support Agent", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600 +}; + +const token = jwt.sign(agentPayload, SDK_SECRET, { algorithm: 'HS256' }); +``` + +## Security + +- Generate tokens server-side only +- Never expose SDK Secret in client code +- Use reasonable expiration times + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/cobrowse-sdk/authorize/ diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization.md new file mode 100644 index 00000000..8e0f6864 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/authorization.md @@ -0,0 +1,5 @@ +# Authorization (Reference) + +Canonical source: + +- [Authorization (official)](authorization-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/environment-variables.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/environment-variables.md new file mode 100644 index 00000000..d9c3b5bb --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/environment-variables.md @@ -0,0 +1,20 @@ +# Zoom Cobrowse SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK identity | Zoom Marketplace -> Cobrowse/Contact Center SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | SDK auth signing | Zoom Marketplace -> Cobrowse/Contact Center SDK app -> App Credentials | +| `COBROWSE_BASE_URL` | Optional | Regional/tenant SDK endpoint override | Cobrowse SDK documentation or tenant provisioning details | + +## Runtime-only values + +- `ZOOM_COBROWSE_SESSION_TOKEN` + +If your implementation mints short-lived tokens, store them in memory/cache only. + +## Notes + +- Keep `ZOOM_SDK_SECRET` server-side. +- Some samples use aliases (`SDK_KEY`, `SDK_SECRET`); normalize internally. diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/error-codes.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/error-codes.md new file mode 100644 index 00000000..8574b439 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/error-codes.md @@ -0,0 +1,6 @@ +# Error Codes + +Use the official Cobrowse docs and API behavior notes for code-level troubleshooting. + +- [Get Started (official)](get-started-official.md) +- [API (official)](api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features-official.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features-official.md new file mode 100644 index 00000000..96574f51 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features-official.md @@ -0,0 +1,111 @@ +# Cobrowse SDK - Features + +Annotations, masking, and advanced features. + +## Overview + +Cobrowse SDK includes features for privacy, collaboration, and customization. + +## Annotations + +Agents can draw and highlight on the shared screen. + +### Enable Annotations + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + features: { + annotations: true + } +}); +``` + +### Annotation Tools + +| Tool | Description | +|------|-------------| +| Pointer | Highlight cursor position | +| Draw | Freehand drawing | +| Highlight | Transparent highlight | +| Arrow | Point to elements | + +## Privacy Masking + +Hide sensitive information from agents. + +### Mask Elements + +```html + + + +
Sensitive content
+``` + +### Mask by CSS Selector + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + masking: { + selectors: [ + '.sensitive-data', + '#credit-card-field', + '[data-private]' + ] + } +}); +``` + +## Bring Your Own PIN (BYOP) + +Use your own PIN system instead of Zoom-generated PINs. + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + byop: { + enabled: true, + pin: 'YOUR_CUSTOM_PIN' + } +}); +``` + +## Session Control + +### End Session + +```javascript +cobrowse.endSession(); +``` + +### Pause/Resume + +```javascript +cobrowse.pause(); +cobrowse.resume(); +``` + +### Events + +```javascript +cobrowse.on('sessionStarted', (session) => { + console.log('Session started:', session.pin); +}); + +cobrowse.on('agentJoined', (agent) => { + console.log('Agent joined:', agent.name); +}); + +cobrowse.on('sessionEnded', () => { + console.log('Session ended'); +}); +``` + +## Resources + +- **Features docs**: https://developers.zoom.us/docs/cobrowse-sdk/add-features/ diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features.md new file mode 100644 index 00000000..7c9725fa --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/features.md @@ -0,0 +1,5 @@ +# Features (Reference) + +Canonical source: + +- [Features (official)](features-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started-official.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started-official.md new file mode 100644 index 00000000..34716243 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started-official.md @@ -0,0 +1,91 @@ +# Cobrowse SDK - Get Started + +Set up collaborative browsing on your website. + +## Overview + +This guide walks through integrating the Cobrowse SDK for customer-initiated sessions. + +## Prerequisites + +1. SDK Universal Credit on your Zoom account +2. SDK Key and Secret +3. Token server for JWT generation + +## Step 1: Get SDK Credentials + +1. In Zoom Workplace, go to **Advanced** → **Zoom CPaaS** → **Manage** +2. Click **Build App** +3. Locate **SDK Key** and **SDK Secret** + +## Step 2: Set Up Token Server + +Generate JWTs server-side to protect your SDK Secret. + +```javascript +const jwt = require('jsonwebtoken'); + +function generateCobrowseToken(userId, userName, roleType) { + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 3600; // 1 hour + + const payload = { + user_id: userId, + app_key: SDK_KEY, + role_type: roleType, // 1 = customer, 2 = agent + user_name: userName, + iat: iat, + exp: exp + }; + + return jwt.sign(payload, SDK_SECRET, { algorithm: 'HS256' }); +} +``` + +## Step 3: Integrate Customer SDK + +Add to your website: + +```html + + + + + +``` + +## Step 4: Set Up Agent View + +Agents join via iframe: + +```html + +``` + +## Next Steps + +- Configure [privacy masking](features.md#masking) +- Set up [annotations](features.md#annotations) +- Implement [Bring Your Own PIN](features.md#byop) + +## Resources + +- **Cobrowse docs**: https://developers.zoom.us/docs/cobrowse-sdk/get-started/ diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started.md new file mode 100644 index 00000000..db6d6831 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/get-started.md @@ -0,0 +1,5 @@ +# Get Started (Reference) + +Canonical source: + +- [Get Started (official)](get-started-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/session-events.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/session-events.md new file mode 100644 index 00000000..a99f29c2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/session-events.md @@ -0,0 +1,5 @@ +# Session Events Reference + +Event names and payload behavior are covered in official Cobrowse API documentation. + +- [API (official)](api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/references/settings-reference.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/settings-reference.md new file mode 100644 index 00000000..d3529aa6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/references/settings-reference.md @@ -0,0 +1,6 @@ +# Settings Reference + +Initialization and runtime settings are documented in the official Cobrowse references. + +- [Features (official)](features-official.md) +- [API (official)](api-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md new file mode 100644 index 00000000..aa595c6a --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md @@ -0,0 +1,7 @@ +# Browser Compatibility + +Validate your supported browser matrix and test privacy/cookie constraints that may affect sessions. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..34554d70 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/common-issues.md @@ -0,0 +1,58 @@ +# Common Issues + +Quick diagnostics for Zoom CoBrowse SDK issues. + +- Ensure SDK script/package is loaded. +- Verify role-specific JWT generation on server. +- Validate token expiry and clock skew. +- Confirm session PIN flow between customer and agent. + +## Docs Links / 404s + +**Symptom**: Official doc links you found are stale or return 404. + +**Fix**: +- Prefer the curated references under `references/` (these are meant to stay stable even if external URLs drift). +- If you need working code, start from official sample repos referenced by the skill, then adapt to your stack. + +## Confusing "Who Creates the Session?" + +**Symptom**: You built an "agent creates session" endpoint, but the customer flow seems to actually start the share / generate the PIN. + +**Fix**: +- Treat **customer start/share** as the action that creates the shareable context (PIN/session), then the **agent joins** using that PIN/session info. +- Keep your server responsibilities narrow: token minting, optional auditing, and routing; avoid inventing "session creation" semantics that the SDK already owns. + +## Two PIN Values (Most Common Integration Mistake) + +**Symptom**: UI shows one PIN from backend/session record and another PIN from SDK event, agent gets `Pin not found` or `Cobrowse code not found`. + +**Fix**: +- Treat `session.on("pincode_updated")` as the **authoritative support PIN** for agent entry. +- Display exactly one primary PIN in UI (label it clearly as "Support PIN"). +- Do not surface provisional/debug PINs to users. +- When opening agent page with `?pin=...`, prefer freshly generated links and avoid stale bookmarks. + +## Agent Desk Error `30308` (Pincode is not found) + +**Symptom**: Zoom-hosted agent desk shows: +- `Cobrowse code not found` +- error code `30308` + +**Fix**: +- Ensure customer session is active and not expired before agent joins. +- Use the latest PIN emitted by `pincode_updated`. +- If your app restarts or uses in-memory state, persist session/PIN mapping or avoid strict local PIN gating for desk launch. +- Have agent re-enter a fresh PIN from a newly started customer session. + +## Plain HTML / Express Integration Friction + +**Symptom**: Quickstarts assume Vite/modern build pipeline; your plain HTML/Express adaptation breaks. + +**Fix**: +- Load the SDK exactly as the official snippet expects (script order matters). +- Avoid bundler-only patterns in plain HTML (ESM imports, `import.meta`, etc.) unless you add a bundler. + +See: +- [Get Started](../get-started.md) +- [Get Started (official)](../references/get-started-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/cors-csp.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/cors-csp.md new file mode 100644 index 00000000..ea375778 --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/cors-csp.md @@ -0,0 +1,11 @@ +# CORS and CSP + +For browser integrations: + +- allow required Zoom domains in CSP, +- avoid blocking SDK/script origins, +- validate iframe embedding and cross-origin constraints. + +See: +- [Get Started](../get-started.md) +- [Get Started (official)](../references/get-started-official.md) diff --git a/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/error-codes.md b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/error-codes.md new file mode 100644 index 00000000..7f7cb56a --- /dev/null +++ b/partner-built/zoom-plugin/skills/cobrowse-sdk/troubleshooting/error-codes.md @@ -0,0 +1,7 @@ +# Error Codes Troubleshooting + +Use official API guidance and startup diagnostics to map error behavior. + +See: +- [Error Codes Reference](../references/error-codes.md) +- [API (official)](../references/api-official.md) diff --git a/partner-built/zoom-plugin/skills/contact-center/RUNBOOK.md b/partner-built/zoom-plugin/skills/contact-center/RUNBOOK.md new file mode 100644 index 00000000..8041edf7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/RUNBOOK.md @@ -0,0 +1,73 @@ +# Contact Center 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Zoom Contact Center integration failures quickly. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is a navigation convention for larger skill docs. + +## 1) Confirm Integration Path + +- Contact Center app inside Zoom client: use Zoom Apps SDK APIs/events (`getEngagementContext`, `onEngagementStatusChange`, etc.). +- Website embed: use Contact Center web SDK/campaign script path. +- Native mobile app: use Android/iOS Contact Center SDK binaries and service lifecycle. + +Wrong path is the top source of confusion. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA channels. +- `apiKey` for scheduled callback and campaign/web-tag scenarios. +- If building a Contact Center app in Zoom client, validate app credentials and OAuth setup in Marketplace. + +## 3) Confirm Lifecycle Order + +Common native/mobile order: +1. Initialize SDK context early. +2. Get service instance. +3. Initialize service with `ZoomCCItem`. +4. Register listener/delegate. +5. `login()` where required (typically chat/ZVA). +6. `fetchUI()` to present the channel view. + +Web app path: +1. `zoomSdk.config(...)` +2. `getEngagementContext()` and `getEngagementStatus()` +3. subscribe to `onEngagementContextChange` and `onEngagementStatusChange` +4. persist state keyed by `engagementId` + +## 4) Confirm Context Switching Behavior + +- A single app instance can receive multiple engagement contexts. +- Persist draft/workflow state by `engagementId`. +- Do not assume only one active engagement for chat/SMS/email workflows. + +## 5) Confirm Cleanup Semantics + +- End action (`endChat`, `endVideo`, `endScheduledCallback`) is not the same as service release. +- Apply platform-specific cleanup (`logout`/`logoff`, release/uninitialize APIs). +- On iOS, forward app lifecycle callbacks (`appDidBecomeActive`, `appWillTerminate`, etc.) to `ZoomCCInterface`. + +## 6) Version + Drift Checks + +- Zoom enforces minimum SDK versions quarterly (first weekend of February, May, August, November). +- Re-check docs and changelog before release; naming and signatures can drift. +- Watch deprecations: + - iOS `onService:error:detail:` is deprecated in favor of `onService:error:detail:description:`. + +## 7) Quick Probes + +- App context/status APIs return valid values. +- Engagement events fire when agent switches engagements. +- Chat/video/scheduled callback can be started and ended once each without stale state. +- No CSP or domain allow-list blocks for web integrations. + +## 8) Fast Decision Tree + +- No engagement data in Contact Center app -> missing SDK `config` capabilities or wrong runtime context. +- Channel UI does not open -> invalid `entryId`/`apiKey`, missing init, or wrong service/channel mapping. +- Events not firing on switch/end -> listeners not attached early enough or removed incorrectly. +- Rejoin fails on mobile -> deep-link/scheme configuration mismatch. + diff --git a/partner-built/zoom-plugin/skills/contact-center/SKILL.md b/partner-built/zoom-plugin/skills/contact-center/SKILL.md new file mode 100644 index 00000000..26564a8f --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/SKILL.md @@ -0,0 +1,121 @@ +--- +name: build-zoom-contact-center-app +description: "Reference skill for Zoom Contact Center. Use after routing to a contact-center workflow when implementing app, web, or native integrations; engagement context and state handling; campaigns; callbacks; or version-drift troubleshooting." +triggers: + - "contact center sdk" + - "zoom contact center" + - "zcc" + - "engagement context" + - "engagement status" + - "campaign sdk" + - "scheduled callback" + - "getengagementcontext" + - "onengagementstatuschange" + - "zoom contact center app" +--- + +# /build-zoom-contact-center-app + +Background reference for Zoom Contact Center integrations across app, web, and native mobile surfaces. + +Implementation guidance for Zoom Contact Center across: +- Contact Center apps in the Zoom client (Zoom Apps SDK path) +- Web channel embeds (chat/video/campaign) +- Native mobile SDKs (Android/iOS) + +Official docs: +- https://developers.zoom.us/docs/contact-center/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +## Routing Guardrail + +- If the user is building an app inside the Zoom Contact Center desktop client, stay on the Zoom Apps SDK path and use this skill plus `zoom-apps-sdk`. +- If the user is embedding chat/video widgets on a website, route to [web/SKILL.md](web/SKILL.md). +- If the user is integrating native Android or iOS SDK binaries, route to [android/SKILL.md](android/SKILL.md) or [ios/SKILL.md](ios/SKILL.md). +- If the user needs Contact Center call-control or queue APIs, chain with [../rest-api/SKILL.md](../rest-api/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +3. [references/forum-top-questions.md](references/forum-top-questions.md) +4. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +5. [references/samples-validation.md](references/samples-validation.md) +6. [references/environment-variables.md](references/environment-variables.md) +7. [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) +8. [RUNBOOK.md](RUNBOOK.md) + +Platform skills: +- [android/SKILL.md](android/SKILL.md) +- [ios/SKILL.md](ios/SKILL.md) +- [web/SKILL.md](web/SKILL.md) + +## Documentation Structure + +``` +contact-center/ +├── SKILL.md +├── RUNBOOK.md +├── concepts/ +│ └── architecture-and-lifecycle.md +├── scenarios/ +│ └── high-level-scenarios.md +├── references/ +│ ├── versioning-and-compatibility.md +│ ├── samples-validation.md +│ └── environment-variables.md +├── troubleshooting/ +│ └── common-drift-and-breaks.md +├── android/ +│ ├── SKILL.md +│ ├── concepts/sdk-lifecycle.md +│ ├── examples/service-patterns.md +│ ├── references/android-reference-map.md +│ └── troubleshooting/common-issues.md +├── ios/ +│ ├── SKILL.md +│ ├── concepts/sdk-lifecycle.md +│ ├── examples/service-patterns.md +│ ├── references/ios-reference-map.md +│ └── troubleshooting/common-issues.md +└── web/ + ├── SKILL.md + ├── concepts/lifecycle-and-events.md + ├── examples/app-context-and-state.md + ├── references/web-reference-map.md + └── troubleshooting/common-issues.md +``` + +## Common Lifecycle Pattern + +1. Initialize platform context early. +2. Build a channel item (`entryId` for chat/video/ZVA, `apiKey` for scheduled callback and campaign flows). +3. Get service/client instance. +4. Register listeners/delegates before user interaction. +5. Start flow (`fetchUI`, `startVideo`, or web SDK open/show path). +6. Handle engagement state changes (`start`, `hold`, `resume`, `end`) and context switching. +7. End flow and release resources (`endChat`/`endVideo`, `logout/logoff`, uninitialize/release). + +## High-Level Scenarios + +- Agent side-panel app that stores notes per `engagementId` and survives context switching. +- Browser chat/video campaigns launched from web tags. +- Native mobile customer app for chat/video/scheduled callback. +- Campaign-driven channel selection (chat, ZVA, video, scheduled callback). +- Rejoin flow for dropped video engagements on mobile. +- Smart Embed CRM softphone with postMessage event contracts. + +See [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) for details. + +## Chaining + +- Auth and in-client app identity: [../zoom-apps-sdk/SKILL.md](../zoom-apps-sdk/SKILL.md) and [../oauth/SKILL.md](../oauth/SKILL.md) +- Contact Center REST workflows: [../rest-api/SKILL.md](../rest-api/SKILL.md) +- Cobrowse on web voice/chat channels: [../cobrowse-sdk/SKILL.md](../cobrowse-sdk/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/contact-center/android/RUNBOOK.md b/partner-built/zoom-plugin/skills/contact-center/android/RUNBOOK.md new file mode 100644 index 00000000..8838dc11 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/RUNBOOK.md @@ -0,0 +1,64 @@ +# Contact Center Android 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm channel target and integration mode for Android. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/android/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/android/` +- `raw-docs/marketplacefront.zoom.us/sdk/contact/android/` diff --git a/partner-built/zoom-plugin/skills/contact-center/android/SKILL.md b/partner-built/zoom-plugin/skills/contact-center/android/SKILL.md new file mode 100644 index 00000000..f2dbb43f --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/SKILL.md @@ -0,0 +1,53 @@ +--- +name: contact-center/android +description: "Zoom Contact Center SDK for Android. Use for native Android chat/video/ZVA/scheduled callback integrations, campaign mode, service lifecycle, and rejoin handling." +user-invocable: false +triggers: + - "contact center android" + - "zcc android" + - "zoomccinterface android" + - "zoomccchatservice" + - "zoomccvideoservice" + - "releasezoomccservice" + - "android rejoin" +--- + +# Zoom Contact Center SDK - Android + +Official docs: +- https://developers.zoom.us/docs/contact-center/android/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +## Quick Links + +1. [concepts/sdk-lifecycle.md](concepts/sdk-lifecycle.md) +2. [examples/service-patterns.md](examples/service-patterns.md) +3. [references/android-reference-map.md](references/android-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## SDK Surface Summary + +- SDK manager: `ZoomCCInterface` +- Channel services: +- `getZoomCCChatService()` +- `getZoomCCVideoService()` +- `getZoomCCZVAService()` +- `getZoomCCScheduledCallbackService()` +- Campaign support via web campaign service and campaign metadata. + +## Hard Guardrails + +- Initialize SDK in `Application.onCreate`. +- Use `ZoomCCItem` to define channel + identifiers. +- Use `entryId` for chat/video/ZVA. +- Use `apiKey` for scheduled callback and campaign mode. +- Release services on teardown. + +## Common Chains + +- Contact Center app and engagement context: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- Contact Center API automation: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/contact-center/android/concepts/sdk-lifecycle.md b/partner-built/zoom-plugin/skills/contact-center/android/concepts/sdk-lifecycle.md new file mode 100644 index 00000000..c8c04b03 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/concepts/sdk-lifecycle.md @@ -0,0 +1,42 @@ +# Android SDK Lifecycle + +## Startup + +1. Initialize once in `Application.onCreate`. +2. Optionally set/update context user name before channel launch. + +## Channel Initialization + +1. Get service from `ZoomCCInterface`. +2. Build `ZoomCCItem` with: +- `sdkType` +- `entryId` or `apiKey` +- `serverType` +- campaign fields when needed +3. `service.init(item)`. +4. Add listener(s). + +## Launch + +1. Chat/ZVA: +- call `login()` then `fetchUI()`. +2. Video: +- configure preview/auto-join options as needed. +- call `fetchUI()`; login is typically internal for video flow. +3. Scheduled callback: +- init with `apiKey`. +- `fetchUI()`. + +## End and Cleanup + +1. End engagement (`endChat` / `endVideo`) when needed. +2. `logoff()` when you need to stop callbacks. +3. `releaseZoomCCService(key)` in teardown paths (`onDestroy`). + +## Campaign Mode + +1. Request campaigns with campaign API key. +2. Select channel from campaign metadata. +3. Reinitialize service using campaign-mode item. +4. Release or end conflicting channel services before switch. + diff --git a/partner-built/zoom-plugin/skills/contact-center/android/examples/service-patterns.md b/partner-built/zoom-plugin/skills/contact-center/android/examples/service-patterns.md new file mode 100644 index 00000000..4d6d3f9d --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/examples/service-patterns.md @@ -0,0 +1,68 @@ +# Android Service Patterns + +## Chat Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCChatService() +service.init( + ZoomCCItem( + entryId = chatEntryId, + sdkType = ZoomCCIInterfaceType.CHAT, + serverType = CCServerType.CCServerWWW + ) +) +service.addListener(object : ZoomCCChatListener { + override fun unreadMsgCountChanged(count: Int) {} + override fun onClientEvent(event: ClientEvent) {} + override fun onEngagementEnd(engagementId: String) {} + override fun onEngagementStart(engagementId: String) {} + override fun onLoginStatus(status: IMStatus?) {} + override fun onError(error: Int, detail: Long, description: String) {} +}) +service.login() +service.fetchUI() +``` + +## Video Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCVideoService() +service.init( + ZoomCCItem( + entryId = videoEntryId, + sdkType = ZoomCCIInterfaceType.VIDEO, + serverType = CCServerType.CCServerWWW + ) +) +service.setVideoPreviewOption(VideoPreviewOption.ZmCCVideoPreviewOptionDefault) +service.setAutoJoinWhenVideoCreated(false) +service.setUseBackwardFacingCameraByDefault(false) +service.addListener(object : ZoomCCVideoListener {}) +service.fetchUI() +``` + +## Scheduled Callback Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCScheduledCallbackService() +service.init( + ZoomCCItem( + apiKey = callbackApiKey, + sdkType = ZoomCCIInterfaceType.SCHEDULED_CALLBACK, + serverType = CCServerType.CCServerWWW + ) +) +service.fetchUI() +``` + +## Cleanup Pattern + +```kotlin +override fun onDestroy() { + ZoomCCInterface.releaseZoomCCService(chatEntryId) + ZoomCCInterface.releaseZoomCCService(videoEntryId) + ZoomCCInterface.releaseZoomCCService(callbackApiKey) + super.onDestroy() +} +``` + diff --git a/partner-built/zoom-plugin/skills/contact-center/android/references/android-reference-map.md b/partner-built/zoom-plugin/skills/contact-center/android/references/android-reference-map.md new file mode 100644 index 00000000..444092dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/references/android-reference-map.md @@ -0,0 +1,51 @@ +# Android Reference Map + +Primary reference: +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +## Core Types + +- `ZoomCCInterface` +- `ZoomCCItem` +- `ZoomCCContext` +- `ZoomCCService` +- `ZoomCCChatService` +- `ZoomCCVideoService` +- `ZoomCCScheduledCallbackService` + +## Listener Types + +- `ZoomCCServiceListener` +- `ZoomCCChatListener` +- `ZoomCCVideoListener` + +## Enums + +- `ZoomCCIInterfaceType` +- `ClientEvent` +- `IMStatus` +- `CCServerType` +- `VideoPreviewOption` + +## Common Methods + +- SDK init/context: +- `ZoomCCInterface.init(...)` +- `ZoomCCInterface.setContext(...)` +- service factory: +- `getZoomCCChatService()` +- `getZoomCCVideoService()` +- `getZoomCCZVAService()` +- `getZoomCCScheduledCallbackService()` +- service lifecycle: +- `init(item)`, `login()`, `logoff()`, `fetchUI()` +- engagement control: +- `endChat()`, `endVideo()` +- release: +- `releaseZoomCCService(key)` + +## Deprecation Notes + +- Review `deprecated.html` in each SDK version package. +- Keep runtime guards for enum/value additions and optional callbacks. + diff --git a/partner-built/zoom-plugin/skills/contact-center/android/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/contact-center/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..7de43da8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/android/troubleshooting/common-issues.md @@ -0,0 +1,44 @@ +# Android Common Issues + +## SDK Works Inconsistently Across Screens + +Cause: +- SDK initialized too late. + +Fix: +- Initialize in `Application.onCreate`. + +## `NoClassDefFoundError` / viewBinding Errors + +Cause: +- Missing expected dependencies or view binding configuration. + +Fix: +- Match SDK package module requirements. +- Ensure build config aligns with current SDK release notes. + +## Video/Chat UI Does Not Open + +Cause: +- Wrong identifier type in `ZoomCCItem`. + +Fix: +- `entryId` for chat/video/ZVA. +- `apiKey` for scheduled callback/campaign. + +## Events Not Firing + +Cause: +- Listener attached after service launch or removed early. + +Fix: +- Add listeners before `fetchUI`. + +## Rejoin Link Opens Browser But Not App + +Cause: +- Deep-link host/scheme mismatch. + +Fix: +- Align Android manifest intent filters with generated rejoin URL format. + diff --git a/partner-built/zoom-plugin/skills/contact-center/concepts/architecture-and-lifecycle.md b/partner-built/zoom-plugin/skills/contact-center/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..74f71c46 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/concepts/architecture-and-lifecycle.md @@ -0,0 +1,63 @@ +# Contact Center Architecture and Lifecycle + +This document defines a stable architecture pattern that works across Contact Center app, web, and mobile integrations. + +## Architecture Layers + +1. Integration Surface +- Zoom Contact Center App (Zoom client embedded webview). +- Web SDK/Campaign SDK on external sites. +- Android/iOS native SDK. + +2. Engagement State Layer +- Current `engagementId`. +- Engagement status (`start`, `hold`, `resume`, `end`). +- Engagement-scoped draft data. + +3. Channel Service Layer +- Chat. +- Video. +- ZVA. +- Scheduled Callback. + +4. Persistence Layer +- Transient per-engagement state cache (frontend local storage or backend session store). +- Optional backend persistence for long-running workflows and compliance logging. + +## Canonical Lifecycle + +1. Initialize context. +2. Determine active engagement context. +3. Build/init channel service/client. +4. Register callbacks before launching UI. +5. Start channel view. +6. Process status/context events. +7. End and cleanup. + +## Context-Switching Contract + +- Treat `engagementId` as the primary state key. +- Never assume a single engagement in memory for messaging channels. +- Restore state on each engagement context change. +- Clear or archive engagement state only when end-state logic is complete. + +## Event-Driven Contract + +- Do not poll as a primary strategy. +- Subscribe early and keep handlers idempotent. +- Handle out-of-order or repeated events safely. + +## Campaign Mode Pattern + +1. Fetch campaigns with campaign API key. +2. Pick channel from `translatedCampaignChannels`. +3. Create channel item with `useCampaignMode=true`. +4. Launch service UI. +5. Release conflicting channel services when switching channels. + +## Security and Identity + +- Use explicit user/session identity refresh paths (`authorize`, `getAppContext`) for Contact Center app scenarios. +- For PWA flows, do not depend on `x-zoom-app-context` header. +- Keep OAuth and app context decryption on backend where possible. + diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/RUNBOOK.md b/partner-built/zoom-plugin/skills/contact-center/ios/RUNBOOK.md new file mode 100644 index 00000000..20fadf2e --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/RUNBOOK.md @@ -0,0 +1,64 @@ +# Contact Center iOS 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm channel target and integration mode for iOS. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/ios/ +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/ios/` +- `raw-docs/marketplacefront.zoom.us/sdk/contact/ios/` diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/SKILL.md b/partner-built/zoom-plugin/skills/contact-center/ios/SKILL.md new file mode 100644 index 00000000..36c84cf1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/SKILL.md @@ -0,0 +1,52 @@ +--- +name: contact-center/ios +description: "Zoom Contact Center SDK for iOS. Use for native iOS chat/video/ZVA/scheduled callback integrations, app lifecycle bridging, rejoin flow, and callback handling." +user-invocable: false +triggers: + - "contact center ios" + - "zcc ios" + - "zoomccinterface ios" + - "handleRejoinVideoOpenURL" + - "zoomccservicedelegate" + - "scheduled callback ios" +--- + +# Zoom Contact Center SDK - iOS + +Official docs: +- https://developers.zoom.us/docs/contact-center/ios/ +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +## Quick Links + +1. [concepts/sdk-lifecycle.md](concepts/sdk-lifecycle.md) +2. [examples/service-patterns.md](examples/service-patterns.md) +3. [references/ios-reference-map.md](references/ios-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## SDK Surface Summary + +- Manager: `ZoomCCInterface.sharedInstance()` +- Context: `ZoomCCContext` +- Items: `ZoomCCItem` +- Services: +- `chatService` +- `zvaService` +- `videoService` +- `scheduledCallbackService` + +## Hard Guardrails + +- Set `ZoomCCContext` before channel operations. +- Forward app lifecycle calls (`appDidBecomeActive`, `appDidEnterBackgroud`, `appWillResignActive`, `appWillTerminate`). +- Use item-based initialization for channels. +- Keep rejoin URL handling connected to the video service path. + +## Common Chains + +- Contact Center apps in Zoom client: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- OAuth and identity: [../../oauth/SKILL.md](../../oauth/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/concepts/sdk-lifecycle.md b/partner-built/zoom-plugin/skills/contact-center/ios/concepts/sdk-lifecycle.md new file mode 100644 index 00000000..ca867fa0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/concepts/sdk-lifecycle.md @@ -0,0 +1,46 @@ +# iOS SDK Lifecycle + +## Context Initialization + +1. Create `ZoomCCContext`. +2. Configure user name, cache folder, and optional share settings. +3. Set context on `ZoomCCInterface.sharedInstance()`. + +## Service Initialization Pattern + +1. Build `ZoomCCItem`. +2. Select channel type: +- `.chat` +- `.video` +- `.ZVA` +- `.scheduledCallback` +3. Populate `entryId` or `apiKey` depending on channel. +4. Get service instance. +5. Set delegate. +6. Call `initialize(with:)`. +7. Call `login()` where required. +8. `fetchUI` and push returned view controller. + +## Lifecycle Bridging + +Forward these app delegate callbacks: +- `applicationDidBecomeActive` -> `appDidBecomeActive` +- `applicationWillResignActive` -> `appWillResignActive` +- `applicationDidEnterBackground` -> `appDidEnterBackgroud` +- `applicationWillTerminate` -> `appWillTerminate` + +## Rejoin Flow + +1. Configure app URL scheme and admin rejoin URL. +2. Forward `open url` callback to rejoin handler. +3. Call video service rejoin API with prepared `ZoomCCItem`. +4. Push returned view controller in completion block. + +## Cleanup + +- End service-specific engagement methods: +- `endChat` +- `endVideo` +- `endScheduledCallback` +- Use service `logout` / uninitialize patterns when needed by flow design. + diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/examples/service-patterns.md b/partner-built/zoom-plugin/skills/contact-center/ios/examples/service-patterns.md new file mode 100644 index 00000000..af2cb950 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/examples/service-patterns.md @@ -0,0 +1,72 @@ +# iOS Service Patterns + +## Context Setup + +```swift +let context = ZoomCCContext() +context.cacheFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! +context.userName = userName +context.domainType = .US01 +ZoomCCInterface.sharedInstance().context = context +``` + +## Chat Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .chat +item.entryId = chatEntryId + +let chat = ZoomCCInterface.sharedInstance().chatService() +chat.chatDelegate = self +if chat.status == .initial { + chat.initialize(with: item) + chat.login() +} +chat.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Video Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .video +item.entryId = videoEntryId + +let video = ZoomCCInterface.sharedInstance().videoService() +video.videoDelegate = self +if video.status == .initial { + video.initialize(with: item) +} +video.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Scheduled Callback Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .scheduledCallback +item.apiKey = callbackApiKey + +let scheduled = ZoomCCInterface.sharedInstance().scheduledCallbackService() +scheduled.scheduledCallbackDelegate = self +if scheduled.status == .initial { + scheduled.initialize(with: item) +} +scheduled.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Rejoin Pattern + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + return rootVC.handleRejoinVideoOpenURL(url) +} +``` + diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/references/ios-reference-map.md b/partner-built/zoom-plugin/skills/contact-center/ios/references/ios-reference-map.md new file mode 100644 index 00000000..211abd99 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/references/ios-reference-map.md @@ -0,0 +1,48 @@ +# iOS Reference Map + +Primary references: +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html +- SDK headers packaged in iOS SDK zip (`ZoomCCInterface.h`) + +## Core Types + +- `ZoomCCInterface` +- `ZoomCCContext` +- `ZoomCCItem` +- `ZoomCCCampaignInfo` + +## Service Protocols + +- `ZoomCCService` +- `ZoomCCChatService` +- `ZoomCCVideoService` +- `ZoomCCScheduledCallbackService` + +## Delegate Protocols + +- `ZoomCCServiceDelegate` +- `ZoomCCChatServiceDelegate` +- `ZoomCCAppLifecyleDelegate` + +## Key Methods + +- Interface: +- `sharedInstance` +- `chatService` +- `zvaService` +- `videoService` +- `scheduledCallbackService` +- `getCampaigns` +- Service lifecycle: +- `initializeWithItem` +- `login` +- `logout` +- `fetchUI` +- Video: +- `handleRejoinVideoOpenURL:item:videoDelegate:complete:` + +## Deprecation Note + +- `onService:error:detail:` is deprecated. +- Use `onService:error:detail:description:`. + diff --git a/partner-built/zoom-plugin/skills/contact-center/ios/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/contact-center/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..d43d9721 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/ios/troubleshooting/common-issues.md @@ -0,0 +1,42 @@ +# iOS Common Issues + +## Service Starts But View Never Appears + +Cause: +- Missing `fetchUI` handling or wrong navigation presentation path. + +Fix: +- Ensure returned view controller is pushed/presented on main thread. + +## App Background/Foreground Breaks Session + +Cause: +- App lifecycle callbacks not forwarded to SDK. + +Fix: +- Wire app delegate lifecycle methods to `ZoomCCInterface`. + +## Rejoin URL Arrives But Rejoin Fails + +Cause: +- URL scheme mismatch or context not initialized. + +Fix: +- Verify URL types config, rejoin URL settings, and context setup before calling rejoin API. + +## Duplicate or Stale Channel Sessions + +Cause: +- Previous service instance left active during channel switches. + +Fix: +- End current engagement and rebuild service item when changing channel/campaign context. + +## Error Callback Signature Drift + +Cause: +- Implemented only deprecated callback signature. + +Fix: +- Implement `onService:error:detail:description:` and keep compatibility wrappers as needed. + diff --git a/partner-built/zoom-plugin/skills/contact-center/references/environment-variables.md b/partner-built/zoom-plugin/skills/contact-center/references/environment-variables.md new file mode 100644 index 00000000..1fc11af4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/references/environment-variables.md @@ -0,0 +1,25 @@ +# Zoom Contact Center Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes (API/OAuth integrations) | OAuth app identity for Contact Center APIs | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes (API/OAuth integrations) | OAuth token exchange | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | User OAuth flow | OAuth callback URL | Zoom Marketplace -> OAuth redirect/allow list | +| `ZCC_CHAT_ENTRY_ID` | Web/chat entry flows | Contact Center chat entry point routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_VIDEO_ENTRY_ID` | Video engagement flows | Contact Center video entry point routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_ZVA_ENTRY_ID` | Optional (Virtual Agent) | Virtual agent entry routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_CAMPAIGN_API_KEY` | Campaign/web embed mode | Campaign authorization for web embed | Contact Center Admin -> Campaign Management -> Web and In-App -> Embed Web Tag | +| `ZCC_WEB_API_KEY` | Web SDK/embed mode | Client-side Contact Center embed initialization | Contact Center Admin -> Campaign Management -> Web and In-App -> Embed Web Tag | +| `ZCC_SCHEDULED_CALLBACK_API_KEY` | Scheduled callback flows | Callback scheduling authorization | Contact Center campaign/flow callback configuration | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- Contact/session IDs issued by Contact Center runtime APIs + +## Notes + +- Contact Center implementations often mix OAuth credentials with flow/campaign keys. +- Keep OAuth secrets and campaign keys out of client-side source control. diff --git a/partner-built/zoom-plugin/skills/contact-center/references/forum-top-questions.md b/partner-built/zoom-plugin/skills/contact-center/references/forum-top-questions.md new file mode 100644 index 00000000..5285fcc1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/references/forum-top-questions.md @@ -0,0 +1,82 @@ +--- +title: "Forum-Derived Top Questions (Contact Center)" +--- + +# Forum-Derived Top Questions (Contact Center) + +Use this as a checklist of the most common recent Developer Forum asks for Zoom Contact Center integrations. + +## Fast Routing Questions (Ask First) + +- Surface: Contact Center app in Zoom client, web SDK/campaign embed, Smart Embed, or REST API workflow. +- Runtime: web vs Android vs iOS and exact SDK version. +- Auth context: app type, scopes, token owner, and Contact Center admin role. +- Resource target: queue/flow/engagement IDs and expected channel (`voice`, `video`, `chat`, callback). +- Failure proof: exact endpoint/event, full response code/message, and one representative payload. + +## Smart Embed Login/Origin Problems + +Common asks: +- Login popup completes but embed never receives session. +- Hosted environment fails while local HTML test works. + +Answer pattern: +- Verify allowed domain configuration exactly matches production origin. +- Validate `origin` usage and `postMessage` contract assumptions. +- Check iframe/sandbox/CSP restrictions for hosted environments. +- Reproduce with a minimal page (embed only) to isolate app-layer interference. + +## Token Works for Phone But Contact Center API Returns 401 + +Common asks: +- Same bearer token can call Phone endpoints but Contact Center endpoints return invalid token. + +Answer pattern: +- Confirm Contact Center scopes are on the active token (not only app config). +- Confirm requester has Contact Center admin permissions in target account. +- Confirm account context did not drift (owner/admin reassignment can break behavior). +- Regenerate token after any scope/role changes. + +## Event Gaps and State-Change Confusion + +Common asks: +- `contact_center.user_status_changed` or engagement events appear missing. +- Documented event name does not fire as expected in a given lifecycle. + +Answer pattern: +- Attach listeners before channel/session start. +- Verify event coverage for the specific channel and engagement phase. +- Confirm network/security layers are not blocking webhook deliveries. +- Add reconciliation logic instead of assuming every state transition emits one event. + +## Recordings and Transcripts Edge Cases + +Common asks: +- Recording rows exist but media/transcript is unavailable. +- Transcript download fails or payload differs from expectations. + +Answer pattern: +- Check recording duration/status before download attempts. +- Handle not-ready and no-recording states explicitly. +- Retry with bounded backoff for newly completed engagements. +- Keep fallback handling for empty/partial recording metadata. + +## Analytics Pagination Repeats First Page + +Common asks: +- `next_page_token` loops the same records in historical analytics endpoints. + +Answer pattern: +- Keep all filter params stable while paging. +- Use token exactly as returned; do not mutate sort/filter inputs mid-stream. +- Add duplicate-page detection and stop conditions in client code. + +## Data Availability Boundaries + +Common asks: +- Access to in-progress chat messages or other live interaction internals. + +Answer pattern: +- Distinguish near-real-time events from post-engagement reporting APIs. +- Set expectations early when an in-progress data surface is unavailable. +- Design workflows around available lifecycle events and finalized engagement data. diff --git a/partner-built/zoom-plugin/skills/contact-center/references/samples-validation.md b/partner-built/zoom-plugin/skills/contact-center/references/samples-validation.md new file mode 100644 index 00000000..1b67da5d --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/references/samples-validation.md @@ -0,0 +1,48 @@ +# Samples Validation Summary + +This summary captures lifecycle and architecture checks against these references: + +- Web: +- https://github.com/zoom/ZCC-Zoom-App-Advanced-Sample +- https://github.com/zoom/zcc-javascript-quickstart +- https://github.com/zoom/zcc-nextjs-sample +- iOS package: `ios-zccsdk-5.2.0.zip` +- Android package: `android-zccsdk-5.2.0.zip` + +## Confirmed Lifecycle Patterns + +1. Contact Center App (Zoom Apps SDK): +- Configure capabilities. +- Query engagement context/status. +- Subscribe to engagement change events. +- Persist state by `engagementId`. + +2. Android Native: +- Initialize in `Application.onCreate`. +- Service `init` with `ZoomCCItem`. +- Use `fetchUI` to present channel. +- `logoff` and `releaseZoomCCService` on cleanup. + +3. iOS Native: +- Set `ZoomCCInterface.sharedInstance().context`. +- Initialize service with item. +- Use `fetchUI` to present. +- Forward app lifecycle callbacks to SDK. +- Use rejoin handler path for video reconnect. + +## Contradictions and Drift Signals + +- Some docs show simplified `service.init("EntryId")` signatures while current references emphasize item-based initialization. +- iOS deprecated error callback still appears in older sample/docs. +- Some public sample manifests contain values that conflict with expected Contact Center embedding configuration and should be reviewed per environment. +- Scraped reference pages include parser artifacts (`TODO`/error pages) and should not be treated as canonical API surfaces. + +## Operational Guidance + +- Treat samples as architecture guidance, not immutable source of truth. +- Resolve conflicts in this order: +1. Current official docs. +2. Current platform API reference. +3. Latest shipped SDK headers/binaries. +4. Samples. + diff --git a/partner-built/zoom-plugin/skills/contact-center/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/contact-center/references/versioning-and-compatibility.md new file mode 100644 index 00000000..29c33100 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/references/versioning-and-compatibility.md @@ -0,0 +1,41 @@ +# Versioning and Compatibility Notes + +## Minimum Version Enforcement + +- Zoom enforces SDK minimum versions quarterly. +- Enforcement windows are announced with advance notice. +- Older SDKs can stop functioning in production even if code has not changed. + +## Practical Policy + +1. Track SDK version in runtime telemetry. +2. Maintain a scheduled upgrade cadence. +3. Validate critical flows every release: +- launch/init +- engagement events +- channel open/close +- rejoin (mobile) + +## Known Drift Patterns + +- API shape drift between docs and generated references. +- Legacy snippets showing old method signatures. +- Event naming/style differences between product surfaces. +- Deprecated callbacks preserved for backward compatibility but replaced in newer signatures. + +## iOS Notable Deprecation + +- `onService:error:detail:` is deprecated. +- Prefer `onService:error:detail:description:`. + +## Smart Embed Version Note + +- Smart Embed v3 is the forward path in docs. +- Maintain version-gated integration code if your account still has older embed behavior. + +## Defensive Design + +- Feature-detect methods/events before calling them. +- Keep adapters between your domain model and SDK payloads. +- Avoid hard-coding assumptions about optional fields. + diff --git a/partner-built/zoom-plugin/skills/contact-center/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/contact-center/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..cb54fb8c --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/scenarios/high-level-scenarios.md @@ -0,0 +1,59 @@ +# High-Level Scenarios + +## 1. Agent Notes App in Contact Center + +Goal: +- Agent writes notes that follow engagement context switching. + +Flow: +1. `config` Zoom Apps SDK with engagement capabilities. +2. Load `getEngagementContext` + `getEngagementStatus`. +3. Store notes by `engagementId`. +4. On `onEngagementContextChange`, swap UI state to selected engagement. +5. On `onEngagementStatusChange` `end`, finalize or clear engagement draft. + +## 2. Web Chat Campaign Launch + +Goal: +- Product team controls targeting in admin without code redeploy. + +Flow: +1. Add campaign web tag script. +2. Wait for `zoomCampaignSdk:ready`. +3. Programmatically `open/show/hide/close` as needed. +4. Listen for engagement events for analytics and CRM writes. + +## 3. Mobile Chat and Video with Native SDK + +Goal: +- Customer mobile app can launch chat/video and recover from interruptions. + +Flow: +1. Initialize SDK context in app startup. +2. Build `ZoomCCItem` for channel. +3. Initialize service, attach delegates/listeners, and launch UI. +4. Handle disconnect/rejoin links for video. +5. End flow and release service resources. + +## 4. Campaign-Mode Channel Router + +Goal: +- Runtime selection of chat/video/ZVA/scheduled callback per campaign. + +Flow: +1. Fetch campaigns by API key. +2. Inspect campaign channels. +3. Build channel-specific item with campaign mode. +4. Release previous conflicting service before opening new channel. + +## 5. Smart Embed CRM Integration + +Goal: +- Embed Contact Center softphone in CRM with screen-pop and contact lookup. + +Flow: +1. Load Smart Embed iframe. +2. Handle postMessage events (`zcc-init-config-request`, search, resize, engagement events). +3. Return contact search results and route screen-pop in CRM. +4. Keep feature flags aligned with Smart Embed version path. + diff --git a/partner-built/zoom-plugin/skills/contact-center/troubleshooting/common-drift-and-breaks.md b/partner-built/zoom-plugin/skills/contact-center/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..7cb17c00 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,62 @@ +# Common Drift and Breaks + +## Symptom: Engagement Context Missing + +Likely causes: +- App is not running in Contact Center context. +- Missing SDK capabilities in `config`. +- Identity/context token path is incomplete. + +Checks: +1. Confirm running context. +2. Confirm capabilities include engagement APIs/events. +3. Confirm app manifest and feature toggles. + +## Symptom: Campaign SDK Methods Throw + +Likely causes: +- Calling methods before `zoomCampaignSdk:ready`. +- Invalid API key or missing campaign configuration. +- Script blocked by CSP/ad-blockers/tag-manager path. + +Checks: +1. Add ready gate before method calls. +2. Validate key/env and script URL. +3. Validate CSP/domain allow lists. + +## Symptom: Native Service Not Responding + +Likely causes: +- SDK init executed too late. +- Wrong channel item (`entryId` vs `apiKey` mismatch). +- Listeners/delegates attached after service start. + +Checks: +1. Move init earlier in app lifecycle. +2. Validate item/channel pairing. +3. Register listeners before `fetchUI`. + +## Symptom: Rejoin Flow Fails + +Likely causes: +- Deep link scheme/host mismatch. +- Rejoin URL or web relay page not configured. +- App lifecycle hooks/context not initialized. + +Checks: +1. Verify platform URL/deep link configuration. +2. Verify admin rejoin settings. +3. Verify rejoin handler wiring. + +## Symptom: Behavior Changed After Release + +Likely causes: +- Minimum version enforcement date reached. +- Deprecated callback removed or changed. +- New SDK defaults in channel behavior. + +Checks: +1. Confirm SDK version in production. +2. Review changelog/deprecation notes. +3. Add adapter guards for optional fields/methods. + diff --git a/partner-built/zoom-plugin/skills/contact-center/web/RUNBOOK.md b/partner-built/zoom-plugin/skills/contact-center/web/RUNBOOK.md new file mode 100644 index 00000000..fb1103e5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/RUNBOOK.md @@ -0,0 +1,63 @@ +# Contact Center Web 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm channel target and integration mode for Web. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/web/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/web/` diff --git a/partner-built/zoom-plugin/skills/contact-center/web/SKILL.md b/partner-built/zoom-plugin/skills/contact-center/web/SKILL.md new file mode 100644 index 00000000..1b461626 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/SKILL.md @@ -0,0 +1,54 @@ +--- +name: contact-center/web +description: "Zoom Contact Center SDK for Web. Use for web chat/video/campaign embeds, engagement event handling, app-context integrations, and Smart Embed postMessage workflows." +user-invocable: false +triggers: + - "contact center web" + - "zcc web sdk" + - "getengagementcontext web" + - "onengagementcontextchange" + - "contact center smart embed" + - "zcc-init-config-request" +--- + +# Zoom Contact Center SDK - Web + +Official docs: +- https://developers.zoom.us/docs/contact-center/web/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ + +## Quick Links + +1. [concepts/lifecycle-and-events.md](concepts/lifecycle-and-events.md) +2. [examples/app-context-and-state.md](examples/app-context-and-state.md) +3. [references/web-reference-map.md](references/web-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Modes + +1. Contact Center App in Zoom client: +- Zoom Apps SDK engagement APIs/events. + +2. External website embed: +- Campaign SDK/web scripts (`zoomCampaignSdk` pattern). +- Video client initialization pattern. + +3. Smart Embed: +- iframe + `postMessage` event contract. + +## Hard Guardrails + +- For campaign SDK, gate calls behind `zoomCampaignSdk:ready`. +- Persist state by `engagementId`. +- Expect context switching and background app behavior. +- Validate CSP and allow-list settings before debugging logic. + +## Chaining + +- For in-client app APIs and auth flows: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- For identity and OAuth: [../../oauth/SKILL.md](../../oauth/SKILL.md) +- For cobrowse workflow: [../../cobrowse-sdk/SKILL.md](../../cobrowse-sdk/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/contact-center/web/concepts/lifecycle-and-events.md b/partner-built/zoom-plugin/skills/contact-center/web/concepts/lifecycle-and-events.md new file mode 100644 index 00000000..c0672f0a --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/concepts/lifecycle-and-events.md @@ -0,0 +1,45 @@ +# Web Lifecycle and Event Model + +## Contact Center App Runtime (Zoom Client) + +1. Configure SDK capabilities. +2. Read running context. +3. Read engagement context/status. +4. Subscribe to: +- `onEngagementContextChange` +- `onEngagementStatusChange` +- optional variable change events +5. Maintain engagement-scoped state. + +## Web Campaign SDK Runtime + +1. Load script with API key. +2. Wait for `zoomCampaignSdk:ready`. +3. Call methods: +- `open` +- `close` +- `show` +- `hide` +- `endChat` +4. Subscribe/unsubscribe to SDK events. + +## Video Client Runtime + +1. Create client. +2. Initialize with entry identifier and optional metadata. +3. Start video. +4. Handle `video-start` and `video-end` events. + +## Smart Embed Runtime + +1. Load Smart Embed iframe. +2. Listen for `message` events from iframe. +3. Respond to init/search/control requests. +4. Map engagement and contact data to CRM/app entities. + +## State Strategy + +- Key all session data by `engagementId`. +- Keep event handlers re-entrant and idempotent. +- Treat `end` status as cleanup boundary. + diff --git a/partner-built/zoom-plugin/skills/contact-center/web/examples/app-context-and-state.md b/partner-built/zoom-plugin/skills/contact-center/web/examples/app-context-and-state.md new file mode 100644 index 00000000..aeb2a609 --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/examples/app-context-and-state.md @@ -0,0 +1,60 @@ +# Web Example: Engagement-Aware State + +```javascript +await zoomSdk.config({ + version: "0.16.0", + capabilities: [ + "getRunningContext", + "getEngagementContext", + "getEngagementStatus", + "onEngagementContextChange", + "onEngagementStatusChange", + ], +}); + +const stateByEngagement = new Map(); +let currentEngagementId = ""; + +function ensureState(id) { + if (!stateByEngagement.has(id)) { + stateByEngagement.set(id, { notes: "", formDraft: {} }); + } + return stateByEngagement.get(id); +} + +async function hydrate() { + const [ctx, status] = await Promise.all([ + zoomSdk.callZoomApi("getEngagementContext"), + zoomSdk.callZoomApi("getEngagementStatus"), + ]); + currentEngagementId = ctx?.engagementContext?.engagementId || ""; + if (currentEngagementId) ensureState(currentEngagementId); + render(currentEngagementId, status?.engagementStatus?.state); +} + +zoomSdk.addEventListener("onEngagementContextChange", (evt) => { + currentEngagementId = evt?.engagementContext?.engagementId || ""; + if (currentEngagementId) ensureState(currentEngagementId); + render(currentEngagementId); +}); + +zoomSdk.addEventListener("onEngagementStatusChange", (evt) => { + const state = evt?.engagementStatus?.state; + if (state === "end" && currentEngagementId) { + stateByEngagement.delete(currentEngagementId); + } + render(currentEngagementId, state); +}); + +hydrate(); +``` + +## Campaign SDK Ready Gate + +```javascript +window.addEventListener("zoomCampaignSdk:ready", () => { + if (!window.zoomCampaignSdk) return; + window.zoomCampaignSdk.show(); +}); +``` + diff --git a/partner-built/zoom-plugin/skills/contact-center/web/references/web-reference-map.md b/partner-built/zoom-plugin/skills/contact-center/web/references/web-reference-map.md new file mode 100644 index 00000000..c31a797e --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/references/web-reference-map.md @@ -0,0 +1,54 @@ +# Web Reference Map + +Primary docs: +- https://developers.zoom.us/docs/contact-center/web/get-started/ +- https://developers.zoom.us/docs/contact-center/web/chat/ +- https://developers.zoom.us/docs/contact-center/web/video/ +- https://developers.zoom.us/docs/contact-center/web/campaigns/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ +- https://developers.zoom.us/docs/contact-center/smart-embed/ + +## Engagement APIs/Events (Contact Center App) + +- `getEngagementContext` +- `getEngagementStatus` +- `onEngagementContextChange` +- `onEngagementStatusChange` +- `onEngagementVariableValueChange` + +## Campaign SDK Events + +- `open` +- `close` +- `show` +- `hide` +- `engagement_started` +- `engagement_ended` + +## Campaign SDK Methods + +- `open()` +- `close()` +- `show()` +- `hide()` +- `endChat()` +- `waitForInit()` +- `waitForReady()` +- `updateUserContext()` + +## Video Client Events + +- `video-start` +- `video-end` +- `notification-join-call` +- `video-click-end` +- `video-force-end` +- `task-created` + +## Smart Embed Event Surface + +- init/config events (`zcc-init-config-request`, `zcc-init-config-response`) +- engagement and channel events +- contact search request/response patterns +- resize and interaction events + diff --git a/partner-built/zoom-plugin/skills/contact-center/web/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/contact-center/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..e8eacd4c --- /dev/null +++ b/partner-built/zoom-plugin/skills/contact-center/web/troubleshooting/common-issues.md @@ -0,0 +1,42 @@ +# Web Common Issues + +## `zoomCampaignSdk` Is Undefined + +Cause: +- Calls happen before readiness event. + +Fix: +- Wait for `zoomCampaignSdk:ready` before calling SDK methods. + +## Widget Does Not Load + +Cause: +- CSP or domain allow-list blocks script/network access. + +Fix: +- Update CSP headers and Marketplace domain allow list entries. + +## App Context Header Missing in PWA + +Cause: +- PWA path does not provide `x-zoom-app-context` header consistently. + +Fix: +- Use `getAppContext()` and backend token decryption flow. + +## Engagement Data Gets Overwritten + +Cause: +- State keyed globally instead of by `engagementId`. + +Fix: +- Persist and restore state per engagement key. + +## Smart Embed Events Not Received + +Cause: +- postMessage listener origin/type filtering missing or incorrect. + +Fix: +- Implement strict message handling and respond to required init/search events. + diff --git a/partner-built/zoom-plugin/skills/debug-zoom-integration/SKILL.md b/partner-built/zoom-plugin/skills/debug-zoom-integration/SKILL.md new file mode 100644 index 00000000..166b9b27 --- /dev/null +++ b/partner-built/zoom-plugin/skills/debug-zoom-integration/SKILL.md @@ -0,0 +1,42 @@ +--- +name: debug-zoom-integration +description: Debug broken Zoom implementations quickly. Use when auth, webhooks, SDK joins, MCP transport, or real-time media workflows are failing and you need to isolate the layer before proposing a fix. +user-invocable: false +--- + +# Debug Zoom Integration + +Use this skill when the user already built something and it is failing. + +## Triage Order + +1. Auth and app configuration +2. Request construction or event verification +3. SDK initialization or platform mismatch +4. Media/session behavior +5. MCP transport and capability assumptions + +## Evidence To Request + +- Exact error text +- Platform and SDK/runtime +- Relevant request or payload sample +- What worked versus what failed +- Whether the issue is reproducible or intermittent + +## Reference Routing + +- [oauth](../oauth/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) +- [rtms](../rtms/SKILL.md) +- [zoom-mcp](../zoom-mcp/SKILL.md) + +## Output + +- Most likely failing layer +- Ranked hypotheses +- Short fix plan +- Verification steps diff --git a/partner-built/zoom-plugin/skills/debug-zoom/SKILL.md b/partner-built/zoom-plugin/skills/debug-zoom/SKILL.md new file mode 100644 index 00000000..979148f1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/debug-zoom/SKILL.md @@ -0,0 +1,39 @@ +--- +name: debug-zoom +description: Debug a broken Zoom integration by isolating the failure point and routing into the right Zoom references. Use when auth, API, webhook, SDK, or MCP behavior is failing and you need a ranked hypothesis list plus verification steps. +argument-hint: "" +--- + +# /debug-zoom + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Debug Zoom auth, API, webhook, SDK, or MCP issues without wandering through the entire docs set. + +## Usage + +```text +/debug-zoom $ARGUMENTS +``` + +## Workflow + +1. Identify the failing layer: auth, API request, webhook, SDK init, media/session behavior, or MCP transport. +2. Ask for the minimum missing evidence: exact error, platform, request/response, event payload, or code path. +3. Produce 2-4 plausible causes ranked by likelihood. +4. Route to the most relevant deep references in `skills/`. +5. Give a short verification plan so the user can confirm the fix. + +## Output + +- Most likely failure layer +- Ranked hypotheses +- Targeted fix steps +- Verification checklist +- Relevant skill links + +## Related Skills + +- [debug-zoom-integration](../debug-zoom-integration/SKILL.md) +- [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) +- [design-mcp-workflow](../design-mcp-workflow/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/design-mcp-workflow/SKILL.md b/partner-built/zoom-plugin/skills/design-mcp-workflow/SKILL.md new file mode 100644 index 00000000..2a081d13 --- /dev/null +++ b/partner-built/zoom-plugin/skills/design-mcp-workflow/SKILL.md @@ -0,0 +1,31 @@ +--- +name: design-mcp-workflow +description: Design a Zoom MCP workflow for Claude. Use when deciding whether Zoom MCP fits a task, when planning tool-based AI workflows, or when separating MCP responsibilities from REST API responsibilities. +user-invocable: false +--- + +# Design MCP Workflow + +Use this skill when the user wants Claude or another MCP-capable client to interact with Zoom via tool calls instead of only deterministic API code. + +## Covers + +- MCP fit assessment +- REST API vs MCP boundaries +- Hybrid architectures +- Connector expectations +- Whiteboard-specific MCP routing + +## Workflow + +1. Decide whether the problem is agentic tooling, deterministic automation, or both. +2. Route MCP-only tasks to [zoom-mcp](../zoom-mcp/SKILL.md). +3. Route hybrid tasks to both [zoom-mcp](../zoom-mcp/SKILL.md) and [rest-api](../rest-api/SKILL.md). +4. If Whiteboard is central, route to [zoom-mcp/whiteboard](../zoom-mcp/whiteboard/SKILL.md). +5. Call out transport, auth, and client capability assumptions explicitly. + +## Common Mistakes + +- Using MCP for deterministic backend jobs that should stay in REST +- Treating MCP as a replacement for all API design +- Ignoring client transport support and auth requirements diff --git a/partner-built/zoom-plugin/skills/general/RUNBOOK.md b/partner-built/zoom-plugin/skills/general/RUNBOOK.md new file mode 100644 index 00000000..c1014193 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/RUNBOOK.md @@ -0,0 +1,65 @@ +# General Skill 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Use `general` as the routing hub for cross-product intent selection. +- Confirm each use-case links to the correct product skill chain. +- Use this runbook before troubleshooting multi-product integrations. + +## 2) Confirm Required Credentials + +- Validate OAuth model selection (User OAuth vs Server-to-Server OAuth) before implementation. +- Ensure required scopes are documented in each use-case. +- Keep credential storage server-side; only expose short-lived tokens to clients. + +## 3) Confirm Lifecycle Order + +1. Pick product path (`REST`, `Meeting SDK`, `Video SDK`, `Apps SDK`, `Phone`, `Contact Center`, etc.). +2. Map auth flow and required scopes. +3. Define event model (`webhooks` or `websockets`) and correlation IDs. +4. Validate deployment model and operational monitoring requirements. + +## 4) Confirm Event/State Handling + +- Keep use-case assumptions explicit when combining multiple products. +- Store cross-system identifiers (meeting/session/call/engagement IDs) for traceability. +- Document fallback behavior when API names/fields drift between versions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Remove stale route links whenever skills are renamed or moved. +- Keep `.env` key references centralized in environment variable reference docs. +- Refresh compatibility notes after each major SDK/API update cycle. + +## 6) Quick Probes + +- Routing matrix still points to existing `SKILL.md` files. +- Use-cases include at least one concrete implementation chain. +- OAuth/scopes guidance matches current Marketplace app model. + +## 7) Fast Decision Tree + +- Unsure between Meeting SDK and Video SDK -> route by UX model (Zoom meeting UI vs fully custom session). +- Need lowest-latency events -> use websockets; otherwise webhooks are acceptable. +- Scope/auth failures in execution -> pause and re-authorize with correct app type and scopes. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/ +- https://marketplace.zoom.us/ +- https://devforum.zoom.us/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/` +- `raw-docs/marketplacefront.zoom.us/sdk/` diff --git a/partner-built/zoom-plugin/skills/general/SKILL.md b/partner-built/zoom-plugin/skills/general/SKILL.md new file mode 100644 index 00000000..b42ccb2e --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/SKILL.md @@ -0,0 +1,320 @@ +--- +name: zoom-general +description: Cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace considerations, or API-vs-MCP routing. +user-invocable: false +triggers: + - "zoom integration" + - "getting started" + - "which zoom sdk" + - "zoom platform" + - "choose zoom api" + - "zoom scopes" + - "marketplace" + - "cross-product" + - "apis vs mcp" + - "api vs mcp" +--- + +# Zoom General (Cross-Product Skills) + +Background reference for cross-product Zoom questions. Prefer the workflow skills first, then use this file for shared platform guidance and routing detail. + +## How `zoom-general` Routes a Complex Developer Query + +Use `zoom-general` as the classifier and chaining layer: + +1. detect product signals in the query +2. pick one primary skill +3. attach secondary skills for auth, events, or deployment edges +4. ask one short clarifier only when two routes match with similar confidence + +Minimal implementation: + +```ts +type SkillId = + | 'zoom-general' + | 'zoom-rest-api' + | 'zoom-webhooks' + | 'zoom-oauth' + | 'zoom-meeting-sdk-web-component-view' + | 'zoom-video-sdk' + | 'zoom-mcp'; + +const hasAny = (q: string, words: string[]) => words.some((w) => q.includes(w)); + +function detectSignals(rawQuery: string) { + const q = rawQuery.toLowerCase(); + return { + meetingCustomUi: hasAny(q, ['zoom meeting', 'custom ui', 'component view', 'embed meeting']), + customVideo: hasAny(q, ['video sdk', 'custom video session', 'peer-video-state-change']), + restApi: hasAny(q, ['rest api', '/v2/', 'create meeting', 'list users', 's2s oauth']), + webhooks: hasAny(q, ['webhook', 'x-zm-signature', 'event subscription', 'crc']), + oauth: hasAny(q, ['oauth', 'pkce', 'token refresh', 'account_credentials']), + mcp: hasAny(q, ['zoom mcp', 'agentic retrieval', 'tools/list', 'semantic meeting search']), + }; +} + +function pickPrimarySkill(s: ReturnType): SkillId { + if (s.meetingCustomUi) return 'zoom-meeting-sdk-web-component-view'; + if (s.mcp) return 'zoom-mcp'; + if (s.restApi) return 'zoom-rest-api'; + if (s.customVideo) return 'zoom-video-sdk'; + return 'zoom-general'; +} + +function buildChain(primary: SkillId, s: ReturnType): SkillId[] { + const chain = [primary]; + if (s.oauth && !chain.includes('zoom-oauth')) chain.push('zoom-oauth'); + if (s.webhooks && !chain.includes('zoom-webhooks')) chain.push('zoom-webhooks'); + return chain; +} +``` + +Example: + +- `Create a meeting, configure webhooks, and handle OAuth token refresh` -> + `zoom-rest-api -> zoom-oauth -> zoom-webhooks` +- `Build a custom video UI for a Zoom meeting on web` -> + `zoom-meeting-sdk-web-component-view` + +For the full TypeScript implementation and handoff contract, use +[references/routing-implementation.md](references/routing-implementation.md). + +## Choose Your Path + +| I want to... | Use this skill | +|--------------|----------------| +| Build a custom web UI around a real Zoom meeting | **[zoom-meeting-sdk-web-component-view](../meeting-sdk/web/component-view/SKILL.md)** | +| Build deterministic automation/configuration/reporting with explicit request control | **[zoom-rest-api](../rest-api/SKILL.md)** | +| Receive event notifications (HTTP push) | **[zoom-webhooks](../webhooks/SKILL.md)** | +| Receive event notifications (WebSocket, low-latency) | **[zoom-websockets](../websockets/SKILL.md)** | +| Embed Zoom meetings in my app | **[zoom-meeting-sdk](../meeting-sdk/SKILL.md)** | +| Build custom video experiences (Web, React Native, Flutter, Android, iOS, macOS, Unity, Linux) | **[zoom-video-sdk](../video-sdk/SKILL.md)** | +| Build an app that runs inside Zoom client | **[zoom-apps-sdk](../zoom-apps-sdk/SKILL.md)** | +| Transcribe uploaded or stored media with AI Services Scribe | **[scribe](../scribe/SKILL.md)** | +| Access live audio/video/transcripts from meetings | **[zoom-rtms](../rtms/SKILL.md)** | +| Enable collaborative browsing for support | **[zoom-cobrowse-sdk](../cobrowse-sdk/SKILL.md)** | +| Build Contact Center apps and channel integrations | **[contact-center](../contact-center/SKILL.md)** | +| Build Virtual Agent web/mobile chatbot experiences | **[virtual-agent](../virtual-agent/SKILL.md)** | +| Build Zoom Phone integrations (Smart Embed, Phone API, webhooks, URI flows) | **[phone](../phone/SKILL.md)** | +| Build Team Chat apps and integrations | **[zoom-team-chat](../team-chat/SKILL.md)** | +| Build server-side integrations with Rivet (auth + webhooks + APIs) | **[rivet-sdk](../rivet-sdk/SKILL.md)** | +| Run browser/device/network preflight diagnostics before join | **[probe-sdk](../probe-sdk/SKILL.md)** | +| Add pre-built UI components for Video SDK | **[zoom-ui-toolkit](../ui-toolkit/SKILL.md)** | +| Implement OAuth authentication (all grant types) | **[zoom-oauth](../oauth/SKILL.md)** | +| Build AI-driven tool workflows (AI Companion/agents) over Zoom data | **[zoom-mcp](../zoom-mcp/SKILL.md)** | +| Build AI-driven Whiteboard workflows over Zoom Whiteboard MCP | **[zoom-mcp/whiteboard](../zoom-mcp/whiteboard/SKILL.md)** | +| Build enterprise AI systems with stable API core + AI tool layer | **[zoom-rest-api](../rest-api/SKILL.md)** + **[zoom-mcp](../zoom-mcp/SKILL.md)** | + +## Planning Checkpoint: Rivet SDK (Optional) + +When a user starts planning a server-side integration that combines auth + webhooks + API calls, ask this first: + +- `Rivet SDK is a Node.js framework that bundles Zoom auth handling, webhook receivers, and typed API wrappers.` +- `Do you want to use Rivet SDK for faster scaffolding, or do you prefer a direct OAuth + REST implementation without Rivet?` + +Routing after answer: + +- If user chooses Rivet: chain `rivet-sdk` + `oauth` + `rest-api`. +- If user declines Rivet: chain `oauth` + `rest-api` (+ `webhooks` or product skill as needed). + +### SDK vs REST Routing Matrix (Hard Stop) + +| User intent | Correct path | Do not route to | +|-------------|--------------|-----------------| +| Embed Zoom meeting in app UI | `zoom-meeting-sdk` | REST-only `join_url` flow | +| Build custom web UI for a real Zoom meeting | `zoom-meeting-sdk-web-component-view` | `zoom-video-sdk` | +| Build custom video UI/session app | `zoom-video-sdk` | Meeting SDK or REST meeting links | +| Get browser join links / manage meeting resources | `zoom-rest-api` | Meeting SDK join implementation | + +Routing guardrails: +- If user asks for SDK embed/join behavior, stay in SDK path. +- If the prompt says **meeting** plus **custom UI/video/layout/embed**, prefer `zoom-meeting-sdk-web-component-view`. +- Only use `zoom-video-sdk` when the user is building a custom session product rather than a Zoom meeting. +- Only use REST path for resource management, reporting, or link distribution unless user explicitly requests a mixed architecture. +- For executable classification/chaining logic and error handling, see [references/routing-implementation.md](references/routing-implementation.md). + +### API vs MCP Routing Matrix (Hard Stop) + +| User intent | Correct path | Why | +|-------------|--------------|-----| +| Deterministic backend automation, account/user configuration, reporting, scheduled jobs | `zoom-rest-api` | Explicit request/response control and repeatable behavior | +| AI agent chooses tools dynamically, cross-platform AI tool interoperability | `zoom-mcp` | MCP is optimized for dynamic tool discovery and agentic workflows | +| Enterprise AI architecture (stable core + adaptive AI layer) | `zoom-rest-api + zoom-mcp` | APIs run core system actions; MCP exposes curated AI tools/context | + +Routing guardrails: +- Do not replace deterministic backend APIs with MCP-only routing. +- Do not force raw REST-first routing when the task is AI-agent tool orchestration. +- Prefer hybrid routing when the user needs both stable automation and AI-driven interactions. +- MCP remote server works over Streamable HTTP/SSE; use this path when the target client/agent supports MCP transports (for example Claude or VS Code). +- Do not design per-tenant custom MCP endpoint provisioning; Zoom MCP endpoints are shared at instance/cluster level. +- Source: https://developers.zoom.us/docs/mcp/library/resources/apis-vs-mcp/ + +### Ambiguity Resolution (Ask Before Routing) + +When a prompt matches both API and MCP paths with similar confidence, ask one short clarifier before execution: + +- `Do you want deterministic REST API automation, AI-agent MCP tooling, or a hybrid of both?` + +Then route as: +- REST answer → `zoom-rest-api` +- MCP answer → `zoom-mcp` +- Hybrid answer → `zoom-rest-api + zoom-mcp` + +### MCP Availability and Topology Notes + +- Zoom-hosted MCP access is evolving; docs indicate a model where Zoom exposes product-scoped MCP servers (for example Meetings, Team Chat, Whiteboard). +- Use `zoom-mcp` as the parent MCP entry point. +- Route Whiteboard-specific MCP requests to **[zoom-mcp/whiteboard](../zoom-mcp/whiteboard/SKILL.md)**. +- When a request is product-specific and MCP coverage exists, route to that MCP product surface first; otherwise use REST/SDK skills for deterministic implementation. + +### Webhooks vs WebSockets + +Both receive event notifications, but differ in approach: + +| Aspect | webhooks | zoom-websockets | +|--------|---------------|-----------------| +| Connection | HTTP POST to your endpoint | Persistent WebSocket | +| Latency | Higher | Lower | +| Security | Requires public endpoint | No exposed endpoint | +| Setup | Simpler | More complex | +| Best for | Most use cases | Real-time, security-sensitive | + +## Common Use Cases + +| Use Case | Description | Skills Needed | +|----------|-------------|---------------| +| [Meeting + Webhooks + OAuth Refresh](references/meeting-webhooks-oauth-refresh-orchestration.md) | Create a meeting, process real-time updates, and refresh OAuth tokens safely in one design | [zoom-rest-api](../rest-api/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) + [zoom-webhooks](../webhooks/SKILL.md) | +| [Scribe Transcription Pipeline](use-cases/scribe-transcription-pipeline.md) | Transcribe uploaded files or S3 archives with AI Services Scribe using fast mode or batch jobs | [scribe](../scribe/SKILL.md) + optional [zoom-rest-api](../rest-api/SKILL.md) + optional [zoom-webhooks](../webhooks/SKILL.md) | +| [APIs vs MCP Routing](use-cases/apis-vs-mcp-routing.md) | Decide whether to route to deterministic Zoom APIs, AI-driven MCP, or a hybrid design | [zoom-rest-api](../rest-api/SKILL.md) and/or [zoom-mcp](../zoom-mcp/SKILL.md) | +| [Custom Meeting UI (Web)](use-cases/custom-meeting-ui-web.md) | Build a custom video UI for a real Zoom meeting in a web app using Meeting SDK Component View | [zoom-meeting-sdk-web-component-view](../meeting-sdk/web/component-view/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Meeting Automation](use-cases/meeting-automation.md) | Schedule, update, delete meetings programmatically | [zoom-rest-api](../rest-api/SKILL.md) | +| [Meeting Bots](use-cases/meeting-bots.md) | Build bots that join meetings for AI/transcription/recording | [meeting-sdk/linux](../meeting-sdk/linux/SKILL.md) + [zoom-rest-api](../rest-api/SKILL.md) + optional [zoom-webhooks](../webhooks/SKILL.md) | +| [High-Volume Meeting Platform](use-cases/high-volume-meeting-platform.md) | Design distributed meeting creation and event processing with retries, queues, and reconciliation | [zoom-rest-api](../rest-api/SKILL.md) + [zoom-webhooks](../webhooks/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Recording & Transcription](use-cases/recording-transcription.md) | Download recordings, get transcripts | [zoom-webhooks](../webhooks/SKILL.md) + [zoom-rest-api](../rest-api/SKILL.md) | +| [Recording Download Pipeline](use-cases/recording-download-pipeline.md) | Auto-download recordings to your own storage (S3, GCS, etc.) | [zoom-webhooks](../webhooks/SKILL.md) + [zoom-rest-api](../rest-api/SKILL.md) | +| [Real-Time Media Streams](use-cases/real-time-media-streams.md) | Access live audio, video, transcripts via WebSocket | [zoom-rtms](../rtms/SKILL.md) + [zoom-webhooks](../webhooks/SKILL.md) | +| [In-Meeting Apps](use-cases/in-meeting-apps.md) | Build apps that run inside Zoom meetings | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [React Native Meeting Embed](use-cases/react-native-meeting-embed.md) | Embed meetings into iOS/Android React Native apps | [zoom-meeting-sdk-react-native](../meeting-sdk/react-native/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Native Meeting SDK Multi-Platform Delivery](use-cases/native-meeting-sdk-multi-platform.md) | Align Android, iOS, macOS, and Unreal Meeting SDK implementations under one auth/version strategy | [zoom-meeting-sdk](../meeting-sdk/SKILL.md) + platform skills | +| [Native Video SDK Multi-Platform Delivery](use-cases/native-video-sdk-multi-platform.md) | Align Android, iOS, macOS, and Unity Video SDK implementations under one auth/version strategy | [zoom-video-sdk](../video-sdk/SKILL.md) + platform skills | +| [Electron Meeting Embed](use-cases/electron-meeting-embed.md) | Embed meetings into desktop Electron apps | [zoom-meeting-sdk-electron](../meeting-sdk/electron/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Flutter Video Sessions](use-cases/flutter-video-sessions.md) | Build custom mobile video sessions in Flutter | [zoom-video-sdk-flutter](../video-sdk/flutter/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [React Native Video Sessions](use-cases/react-native-video-sessions.md) | Build custom mobile video sessions in React Native | [zoom-video-sdk-react-native](../video-sdk/react-native/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Immersive Experiences](use-cases/immersive-experiences.md) | Custom video layouts with Layers API | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) | +| [Collaborative Apps](use-cases/collaborative-apps.md) | Real-time shared state in meetings | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) | +| [Contact Center App Lifecycle and Context Switching](use-cases/contact-center-app-lifecycle-and-context-switching.md) | Build Contact Center apps that handle engagement events and multi-engagement state | [contact-center](../contact-center/SKILL.md) + [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) | +| [Virtual Agent Campaign Web and Mobile Wrapper](use-cases/virtual-agent-campaign-web-mobile-wrapper.md) | Deliver one campaign-driven bot flow across web and native mobile wrappers | [virtual-agent](../virtual-agent/SKILL.md) + [contact-center](../contact-center/SKILL.md) | +| [Virtual Agent Knowledge Base Sync Pipeline](use-cases/virtual-agent-knowledge-base-sync-pipeline.md) | Sync external knowledge content into Zoom Virtual Agent using web sync or custom API connectors | [virtual-agent](../virtual-agent/SKILL.md) + [zoom-rest-api](../rest-api/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) | +| [Zoom Phone Smart Embed CRM Integration](use-cases/zoom-phone-smart-embed-crm.md) | Build CRM dialer and call logging flows using Smart Embed plus Phone APIs | [phone](../phone/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) + [zoom-webhooks](../webhooks/SKILL.md) | +| [Rivet Event-Driven API Orchestrator](use-cases/rivet-event-driven-api-orchestrator.md) | Build a Node.js backend that combines webhooks and API actions through Rivet module clients | [rivet-sdk](../rivet-sdk/SKILL.md) + [zoom-oauth](../oauth/SKILL.md) + [zoom-rest-api](../rest-api/SKILL.md) | +| [Probe SDK Preflight Readiness Gate](use-cases/probe-sdk-preflight-readiness-gate.md) | Add browser/device/network diagnostics and readiness policy before Meeting SDK or Video SDK joins | [probe-sdk](../probe-sdk/SKILL.md) + [zoom-meeting-sdk](../meeting-sdk/SKILL.md) or [zoom-video-sdk](../video-sdk/SKILL.md) | + +## Complete Use-Case Index + +- [APIs vs MCP Routing](use-cases/apis-vs-mcp-routing.md): choose API-only, MCP-only, or hybrid routing using official Zoom criteria. +- [AI Companion Integration](use-cases/ai-companion-integration.md): connect Zoom AI Companion capabilities into your app workflow. +- [AI Integration](use-cases/ai-integration.md): add summarization, transcription, or assistant logic using Zoom data surfaces. +- [Backend Automation (S2S OAuth)](use-cases/backend-automation-s2s-oauth.md): run server-side jobs with account-level OAuth credentials. +- [Collaborative Apps](use-cases/collaborative-apps.md): build shared in-meeting app state and interactions. +- [Contact Center Integration](use-cases/contact-center-integration.md): connect Zoom Contact Center signals into external systems. +- [Contact Center App Lifecycle and Context Switching](use-cases/contact-center-app-lifecycle-and-context-switching.md): implement event-driven engagement state and safe context switching in Contact Center apps. +- [Virtual Agent Campaign Web and Mobile Wrapper](use-cases/virtual-agent-campaign-web-mobile-wrapper.md): deploy campaign-based Virtual Agent chat across website and Android/iOS WebView wrappers. +- [Virtual Agent Knowledge Base Sync Pipeline](use-cases/virtual-agent-knowledge-base-sync-pipeline.md): automate knowledge-base ingestion with web sync strategy or custom API connector. +- [Zoom Phone Smart Embed CRM Integration](use-cases/zoom-phone-smart-embed-crm.md): integrate Smart Embed events, Phone APIs, and CRM workflows with migration-safe data handling. +- [Rivet Event-Driven API Orchestrator](use-cases/rivet-event-driven-api-orchestrator.md): build a Node.js backend that combines webhook handling and API orchestration with Rivet. +- [Probe SDK Preflight Readiness Gate](use-cases/probe-sdk-preflight-readiness-gate.md): run browser/device/network diagnostics before launching meeting or video session workflows. +- [Custom Video](use-cases/custom-video.md): decide between Video SDK and related components for custom session UX. +- [Custom Meeting UI (Web)](use-cases/custom-meeting-ui-web.md): use Meeting SDK Component View for a custom UI around a real Zoom meeting. +- [Scribe Transcription Pipeline](use-cases/scribe-transcription-pipeline.md): use AI Services Scribe for on-demand file transcription and batch archive processing. +- [Video SDK Bring Your Own Storage](use-cases/video-sdk-bring-your-own-storage.md): configure Video SDK cloud recordings to write directly to your own S3 bucket. +- [Customer Support Cobrowsing](use-cases/customer-support-cobrowsing.md): implement customer-agent collaborative browsing support flows. +- [Embed Meetings](use-cases/embed-meetings.md): embed Zoom meeting experience into your app. +- [Form Completion Assistant](use-cases/form-completion-assistant.md): build guided flows for form filling and completion assistance. +- [HD Video Resolution](use-cases/hd-video-resolution.md): enable and troubleshoot high-definition video requirements. +- [High-Volume Meeting Platform](use-cases/high-volume-meeting-platform.md): build distributed meeting creation and event processing with concrete fallback patterns. +- [Immersive Experiences](use-cases/immersive-experiences.md): use Zoom Apps Layers APIs for custom in-meeting visuals. +- [In-Meeting Apps](use-cases/in-meeting-apps.md): build Zoom Apps that run directly inside meeting and webinar contexts. +- [Marketplace Publishing](use-cases/marketplace-publishing.md): prepare and ship a Zoom app through Marketplace review. +- [Meeting Automation](use-cases/meeting-automation.md): create, update, and manage meetings programmatically. +- [Meeting Bots](use-cases/meeting-bots.md): build bots for meeting join, capture, and real-time analysis. +- [Native Meeting SDK Multi-Platform Delivery](use-cases/native-meeting-sdk-multi-platform.md): standardize Android, iOS, macOS, and Unreal Meeting SDK delivery with shared auth and version controls. +- [Native Video SDK Multi-Platform Delivery](use-cases/native-video-sdk-multi-platform.md): standardize Android, iOS, macOS, and Unity Video SDK delivery with shared auth and version controls. +- [Meeting Details with Events](use-cases/meeting-details-with-events.md): combine REST retrieval with webhook event streams. +- [Minutes Calculation](use-cases/minutes-calculation.md): compute usage and minute metrics across meetings/sessions. +- [Prebuilt Video UI](use-cases/prebuilt-video-ui.md): use UI Toolkit for faster Video SDK-based UI delivery. +- [QSS Monitoring](use-cases/qss-monitoring.md): monitor Zoom quality statistics and performance indicators. +- [Raw Recording](use-cases/raw-recording.md): capture raw streams for custom recording and processing pipelines. +- [Electron Meeting Embed](use-cases/electron-meeting-embed.md): embed meetings in an Electron desktop application. +- [Flutter Video Sessions](use-cases/flutter-video-sessions.md): build Video SDK sessions in Flutter mobile apps. +- [React Native Meeting Embed](use-cases/react-native-meeting-embed.md): embed Meeting SDK into React Native apps. +- [React Native Video Sessions](use-cases/react-native-video-sessions.md): build custom video sessions in React Native. +- [Real-Time Media Streams](use-cases/real-time-media-streams.md): consume live media/transcript streams via RTMS. +- [Recording Download Pipeline](use-cases/recording-download-pipeline.md): automate recording retrieval and storage pipelines. +- [Recording & Transcription](use-cases/recording-transcription.md): manage post-meeting recordings and transcript workflows. +- [Retrieve Meeting and Subscribe Events](use-cases/retrieve-meeting-and-subscribe-events.md): join REST meeting fetch with event subscriptions. +- [SaaS App OAuth Integration](use-cases/saas-app-oauth-integration.md): implement user-level OAuth in multi-tenant SaaS apps. +- [SDK Size Optimization](use-cases/sdk-size-optimization.md): reduce bundle/runtime footprint for SDK-based apps. +- [SDK Wrappers and GUI](use-cases/sdk-wrappers-gui.md): evaluate wrapper patterns and GUI frameworks around SDKs. +- [Team Chat LLM Bot](use-cases/team-chat-llm-bot.md): build a Team Chat bot with LLM-powered responses. +- [Testing and Development](use-cases/testing-development.md): local testing patterns, mocks, and safe development loops. +- [Token and Scope Troubleshooting](use-cases/token-and-scope-troubleshooting.md): debug OAuth scope and token mismatch issues. +- [Transcription Bot (Linux)](use-cases/transcription-bot-linux.md): run Linux meeting bots for live transcription workloads. +- [Usage Reporting and Analytics](use-cases/usage-reporting-analytics.md): collect and analyze usage/reporting data. +- [User and Meeting Creation](use-cases/user-and-meeting-creation.md): provision users and schedule meetings in one flow. +- [Web SDK Embedding](use-cases/web-sdk-embedding.md): embed meeting experiences in browser-based web apps. +- [Server-to-Server OAuth with Webhooks](use-cases/server-to-server-oauth-with-webhooks.md): combine account OAuth with event-driven backend processing. +- [Meeting Links vs Embedding](use-cases/meeting-links-vs-embedding.md): choose between `join_url` distribution and SDK embedding. +- [Enterprise App Deployment](use-cases/enterprise-app-deployment.md): deploy, govern, and operate Zoom integrations at enterprise scale. + +## Prerequisites + +1. Zoom account (Pro, Business, or Enterprise) +2. App created in [Zoom App Marketplace](https://marketplace.zoom.us/) +3. OAuth credentials (Client ID and Secret) + +## References + +- [Known Limitations & Quirks](references/known-limitations.md) + +## Quick Start + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select app type (see [references/app-types.md](references/app-types.md)) +4. Configure OAuth and scopes +5. Copy credentials to your application + +## Detailed References + +- **[references/authentication.md](references/authentication.md)** - OAuth 2.0, S2S OAuth, JWT patterns +- **[references/app-types.md](references/app-types.md)** - Decision guide for app types +- **[references/scopes.md](references/scopes.md)** - OAuth scopes reference +- **[references/marketplace.md](references/marketplace.md)** - Marketplace portal navigation +- **[references/query-routing-playbook.md](references/query-routing-playbook.md)** - Route complex queries to the right specialized skills +- **[references/interview-answer-routing.md](references/interview-answer-routing.md)** - Short interview-ready answer pattern for zoom-general routing +- **[references/routing-implementation.md](references/routing-implementation.md)** - Concrete TypeScript query classification and skill handoff contract +- **[references/automatic-skill-chaining-rest-webhooks.md](references/automatic-skill-chaining-rest-webhooks.md)** - Executable process for REST + webhook chained workflows +- **[references/meeting-webhooks-oauth-refresh-orchestration.md](references/meeting-webhooks-oauth-refresh-orchestration.md)** - Concrete design for meeting creation + webhook updates + OAuth token refresh +- **[references/distributed-meeting-fallback-architecture.md](references/distributed-meeting-fallback-architecture.md)** - High-volume distributed architecture with retries, circuit breakers, and reconciliation fallbacks +- **[references/community-repos.md](references/community-repos.md)** - Curated official Zoom sample repositories by product + +## SDK Maintenance + +- **[references/sdk-upgrade-guide.md](references/sdk-upgrade-guide.md)** - Version policy, upgrade steps +- **[references/sdk-upgrade-workflow.md](references/sdk-upgrade-workflow.md)** - Changelog + RSS, version-by-version reusable upgrade workflow +- **[references/sdk-logs-troubleshooting.md](references/sdk-logs-troubleshooting.md)** - Collecting SDK logs + +## Resources + +- **Official docs**: https://developers.zoom.us/ +- **Marketplace**: https://marketplace.zoom.us/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/general/references/app-types.md b/partner-built/zoom-plugin/skills/general/references/app-types.md new file mode 100644 index 00000000..a0852091 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/app-types.md @@ -0,0 +1,106 @@ +# App Types + +Choose the right Zoom app type for your integration. + +## Overview + +Zoom Marketplace has 3 app types: + +| App Type | Use Case | +|----------|----------| +| **General App** | Flexible - configure surfaces, embeds, OAuth, webhooks | +| **Server-to-Server OAuth** | Backend automation, no user authorization | +| **Webhook Only** | Receive events only, no API access | + +## General App + +The modular app type. Pick what you need: + +### OAuth Type (choose one) + +| Type | Scopes | Authorization | +|------|--------|---------------| +| **Admin** | Admin scopes (`*:admin`) | Entire account OR specific users | +| **User** | User scopes | Only themselves (self-service) | + +### Surfaces (product contexts) + +Your app can interact with these Zoom products: + +- Meetings +- Webinars +- Rooms +- Phone +- Team Chat +- Contact Center +- Whiteboard +- Virtual Agent +- Events +- Mail +- Workflows + +### Embeds (SDKs) + +Embed Zoom functionality in your app: + +| Embed | Description | +|-------|-------------| +| **Meeting SDK** | Embed Zoom meetings | +| **Contact Center SDK** | Embed Contact Center | +| **Phone SDK** | Embed Phone functionality | + +### Access + +Configure in the Access tab: + +- **Secret Token** - Verify webhook notifications +- **Event Subscription** - Webhooks +- **WebSockets** - Real-time event connections + +### Scopes + +Define which API methods the app can call. Scopes are: +- Restricted to specific resources +- Reviewed by Zoom during app submission + +### Features in General App + +General App can also include: +- **Zoom Apps** - Apps that run inside Zoom client + +## Server-to-Server OAuth + +Backend automation without user authorization. + +- No user interaction required +- Access your account's data +- Can include webhooks and zoom-websockets +- Best for: automation, reporting, integrations + +## Webhook Only + +Event notifications only. + +- Receive events, no API calls +- No OAuth tokens needed +- Best for: event logging, triggering external workflows + +Use this when you ONLY need events. Otherwise, add webhooks to General App or S2S. + +## Decision Guide + +| Need | App Type | +|------|----------| +| Call APIs for your account (backend) | Server-to-Server OAuth | +| Call APIs on behalf of users | General App (Admin or User OAuth) | +| Embed Zoom meetings | General App + Meeting SDK embed | +| Embed Contact Center | General App + Contact Center SDK embed | +| Embed Phone | General App + Phone SDK embed | +| Build in-client app | General App + Zoom Apps | +| Receive events only | Webhook Only | +| Receive events + call APIs | General App or S2S (with webhooks) | + +## Resources + +- **App types docs**: https://developers.zoom.us/docs/integrations/ +- **Marketplace**: https://marketplace.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/general/references/authentication.md b/partner-built/zoom-plugin/skills/general/references/authentication.md new file mode 100644 index 00000000..275d9d46 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/authentication.md @@ -0,0 +1,82 @@ +# Authentication + +Authentication methods for Zoom APIs and SDKs. + +## Overview + +Zoom supports multiple authentication methods depending on your use case: + +| Method | Use Case | +|--------|----------| +| **OAuth 2.0** | User-authorized access (on behalf of user) | +| **Server-to-Server OAuth** | Server-side automation (no user interaction) | +| **SDK JWT** | Meeting SDK and Video SDK authentication | + +## OAuth 2.0 + +For apps that act on behalf of users. + +### Flow + +``` +1. User clicks "Connect with Zoom" +2. Redirect to Zoom authorization URL +3. User grants permission +4. Zoom redirects back with auth code +5. Exchange code for access token +6. Use token to call APIs +``` + +### Authorization URL + +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri} +``` + +### Token Exchange + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic {base64(clientId:clientSecret)}" \ + -d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}" +``` + +## Server-to-Server OAuth + +For server-side automation without user interaction. + +### Get Access Token + +```bash +curl -X POST "https://zoom.us/oauth/token?grant_type=account_credentials&account_id={accountId}" \ + -H "Authorization: Basic {base64(clientId:clientSecret)}" +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600 +} +``` + +## SDK JWT Signatures + +For Meeting SDK and Video SDK authentication. See: +- [Meeting SDK Authorization](../../meeting-sdk/references/authorization.md) +- [Video SDK Authorization](../../video-sdk/references/authorization.md) + +### Best Practices + +| Practice | Recommendation | +|----------|----------------| +| **Expiry (`exp`)** | Set ~10 seconds after generation | +| **Issued At (`iat`)** | Set 2 hours in the past (if `exp - iat >= 2 hours` required) | +| **Generate server-side** | Never expose secrets in client code | + +## Resources + +- **OAuth docs**: https://developers.zoom.us/docs/integrations/oauth/ +- **S2S OAuth docs**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ diff --git a/partner-built/zoom-plugin/skills/general/references/authorization-patterns.md b/partner-built/zoom-plugin/skills/general/references/authorization-patterns.md new file mode 100644 index 00000000..13acf4b9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/authorization-patterns.md @@ -0,0 +1,562 @@ +# Authorization Patterns + +Permission validation middleware and role-based access control for Zoom API integrations. + +> **Note**: These are **implementation patterns for YOUR application** when building Zoom integrations. These are not Zoom's internal authorization mechanisms - they are examples of how to structure authorization logic in your own backend. + +## Overview + +When chaining multiple Zoom API calls, each step may require different scopes and permissions. This document provides patterns for validating authorization at each step before proceeding. + +## Authorization Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ AUTHORIZATION VALIDATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Check Token Validity │ +│ └── Is token expired? → Refresh or re-authenticate │ +│ └── Is token revoked? → Re-authenticate │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. Validate Required Scopes │ +│ └── Does token have scopes for this operation? │ +│ └── If missing → Return 403 with required scopes │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Check Resource Permissions │ +│ └── Does user have access to this resource? │ +│ └── Is user admin/owner/member? │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 4. Execute Operation │ +│ └── Call Zoom API │ +│ └── Handle API-level authorization errors │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Scope Validation Middleware + +### Express.js Middleware + +```javascript +const axios = require('axios'); + +/** + * Middleware to validate OAuth token has required scopes + * @param {string[]} requiredScopes - Scopes required for this route + */ +function requireScopes(requiredScopes) { + return async (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + error: 'unauthorized', + message: 'No access token provided' + }); + } + + try { + // Get token info to check scopes + const tokenInfo = await getTokenInfo(token); + + // Check if token has all required scopes + const tokenScopes = tokenInfo.scope.split(' '); + const missingScopes = requiredScopes.filter( + scope => !tokenScopes.includes(scope) + ); + + if (missingScopes.length > 0) { + return res.status(403).json({ + error: 'insufficient_scope', + message: 'Token missing required scopes', + required_scopes: requiredScopes, + missing_scopes: missingScopes, + your_scopes: tokenScopes + }); + } + + // Attach token info to request for downstream use + req.zoomToken = tokenInfo; + req.zoomScopes = tokenScopes; + next(); + + } catch (error) { + if (error.response?.status === 401) { + return res.status(401).json({ + error: 'invalid_token', + message: 'Token is invalid or expired' + }); + } + next(error); + } + }; +} + +/** + * Get token information including scopes + * + * IMPORTANT: Scopes are returned during OAuth token exchange, not from API calls. + * You should store the scopes when you receive the access token. + */ +async function getTokenInfo(accessToken) { + // For Server-to-Server OAuth: Decode JWT to get scopes + const parts = accessToken.split('.'); + if (parts.length === 3) { + const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); + return { + scope: payload.scope || '', + exp: payload.exp, + aud: payload.aud + }; + } + + // For User OAuth tokens: Scopes are NOT available from API responses. + // You must store scopes when you receive them during token exchange. + // + // During OAuth token exchange, the response includes: + // { + // "access_token": "...", + // "token_type": "bearer", + // "scope": "user:read meeting:write ...", <-- Store this! + // "expires_in": 3600 + // } + // + // Store the scope in your database alongside the token. + + throw new Error( + 'User OAuth token scopes must be stored during token exchange. ' + + 'Cannot retrieve scopes from an opaque access token.' + ); +} + +/** + * Example: Store scopes during OAuth token exchange + */ +async function handleOAuthCallback(code) { + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code: code, + redirect_uri: REDIRECT_URI + }, + auth: { + username: CLIENT_ID, + password: CLIENT_SECRET + } + }); + + const { access_token, refresh_token, scope, expires_in } = response.data; + + // IMPORTANT: Store the scope along with the token + await saveTokenToDatabase({ + accessToken: access_token, + refreshToken: refresh_token, + scope: scope, // <-- Store this for later permission checks + expiresAt: Date.now() + (expires_in * 1000) + }); + + return { access_token, scope }; +} + +// Usage +const express = require('express'); +const app = express(); + +// Route requiring meeting:read scope +app.get('/api/meetings/:id', + requireScopes(['meeting:read']), + async (req, res) => { + // Token already validated, proceed with API call + const meeting = await getMeeting(req.params.id, req.headers.authorization); + res.json(meeting); + } +); + +// Route requiring multiple scopes +app.post('/api/users/:id/meetings', + requireScopes(['user:read', 'meeting:write']), + async (req, res) => { + const meeting = await createMeeting(req.params.id, req.body, req.headers.authorization); + res.json(meeting); + } +); +``` + +### Scope Requirements by Operation + +| Operation | User Scope | Admin Scope (S2S) | +|-----------|------------|-------------------| +| Get own user info | `user:read` | `user:read:admin` | +| List all users | N/A | `user:read:admin` | +| Create user | N/A | `user:write:admin` | +| Get own meetings | `meeting:read` | `meeting:read:admin` | +| Get any user's meetings | N/A | `meeting:read:admin` | +| Create meeting for self | `meeting:write` | `meeting:write:admin` | +| Create meeting for others | N/A | `meeting:write:admin` | +| List own recordings | `recording:read` | `recording:read:admin` | +| List any user's recordings | N/A | `recording:read:admin` | +| Delete own recording | `recording:write` | `recording:write:admin` | +| Delete any recording | N/A | `recording:write:admin` | +| Access own phone | `phone:read` | `phone:read:admin` | +| Access any user's phone | N/A | `phone:read:admin` | +| Manage phone settings | `phone:write` | `phone:write:admin` | + +> **Note**: "N/A" means this operation requires admin-level scopes and cannot be done with user-level OAuth. + +## Role-Based Access Control + +### Define Roles + +```javascript +/** + * Role definitions with allowed scopes + */ +const ROLES = { + admin: { + scopes: [ + 'user:read:admin', 'user:write:admin', + 'meeting:read:admin', 'meeting:write:admin', + 'recording:read:admin', 'recording:write:admin', + 'account:read:admin', 'account:write:admin' + ], + description: 'Full administrative access' + }, + + manager: { + scopes: [ + 'user:read:admin', + 'meeting:read:admin', 'meeting:write:admin', + 'recording:read:admin' + ], + description: 'Manage meetings and view users' + }, + + user: { + scopes: [ + 'user:read', + 'meeting:read', 'meeting:write', + 'recording:read' + ], + description: 'Manage own meetings and recordings' + }, + + viewer: { + scopes: [ + 'meeting:read', + 'recording:read' + ], + description: 'View-only access' + } +}; + +/** + * Check if user role has required scope + */ +function roleHasScope(role, requiredScope) { + const roleConfig = ROLES[role]; + if (!roleConfig) return false; + + return roleConfig.scopes.some(scope => { + // Exact match + if (scope === requiredScope) return true; + + // Admin scope covers non-admin version + // e.g., meeting:read:admin covers meeting:read + if (scope.endsWith(':admin')) { + const baseScope = scope.replace(':admin', ''); + if (baseScope === requiredScope) return true; + } + + return false; + }); +} + +/** + * Middleware to require a specific role + */ +function requireRole(allowedRoles) { + return (req, res, next) => { + const userRole = req.user?.role; // From your auth system + + if (!userRole || !allowedRoles.includes(userRole)) { + return res.status(403).json({ + error: 'forbidden', + message: 'Insufficient role permissions', + required_roles: allowedRoles, + your_role: userRole || 'none' + }); + } + + next(); + }; +} + +// Usage +app.delete('/api/users/:id', + requireRole(['admin']), + requireScopes(['user:write:admin']), + async (req, res) => { + // Only admins can delete users + await deleteUser(req.params.id); + res.json({ success: true }); + } +); +``` + +## Permission Checking Between Chained Operations + +### Chain Validation Pattern + +```javascript +/** + * Validate permissions for a multi-step operation + * before executing any steps + */ +async function validateChainPermissions(operations, tokenScopes) { + const allRequiredScopes = new Set(); + + for (const op of operations) { + for (const scope of op.requiredScopes) { + allRequiredScopes.add(scope); + } + } + + const missingScopes = [...allRequiredScopes].filter( + scope => !tokenScopes.includes(scope) + ); + + if (missingScopes.length > 0) { + return { + valid: false, + missingScopes, + message: `Cannot complete operation chain. Missing scopes: ${missingScopes.join(', ')}` + }; + } + + return { valid: true }; +} + +/** + * Execute a chain of operations with permission validation + */ +async function executeAuthorizedChain(operations, accessToken) { + // Get token scopes + const tokenInfo = await getTokenInfo(accessToken); + const tokenScopes = tokenInfo.scope.split(' '); + + // Validate all permissions upfront + const validation = await validateChainPermissions(operations, tokenScopes); + if (!validation.valid) { + throw new Error(validation.message); + } + + // Execute operations in sequence + const results = []; + for (const op of operations) { + console.log(`Executing: ${op.name}`); + + try { + const result = await op.execute(accessToken, results); + results.push({ name: op.name, success: true, data: result }); + } catch (error) { + // Check if it's an authorization error + if (error.response?.status === 403) { + throw new Error(`Authorization failed at step "${op.name}": ${error.response.data.message}`); + } + throw error; + } + } + + return results; +} + +// Example: User + Meeting creation chain +const userMeetingChain = [ + { + name: 'createUser', + requiredScopes: ['user:write:admin'], + execute: async (token, previousResults) => { + return await createUser({ + email: 'new@example.com', + firstName: 'New', + lastName: 'User' + }, token); + } + }, + { + name: 'createMeeting', + requiredScopes: ['meeting:write:admin'], + execute: async (token, previousResults) => { + const user = previousResults.find(r => r.name === 'createUser').data; + return await createMeeting(user.id, { + topic: 'Onboarding Meeting' + }, token); + } + } +]; + +// Usage +try { + const results = await executeAuthorizedChain(userMeetingChain, accessToken); + console.log('Chain completed:', results); +} catch (error) { + console.error('Chain failed:', error.message); +} +``` + +## Graceful Degradation + +### Handle Partial Permissions + +```javascript +/** + * Execute with graceful degradation when permissions are partial + */ +async function executeWithDegradation(operations, accessToken) { + const tokenInfo = await getTokenInfo(accessToken); + const tokenScopes = tokenInfo.scope.split(' '); + + const results = []; + + for (const op of operations) { + // Check if we have permission for this operation + const hasPermission = op.requiredScopes.every( + scope => tokenScopes.includes(scope) + ); + + if (!hasPermission) { + if (op.required) { + // Required operation - fail the chain + throw new Error(`Missing required scopes for ${op.name}: ${op.requiredScopes.join(', ')}`); + } else { + // Optional operation - skip with warning + console.warn(`Skipping ${op.name}: insufficient permissions`); + results.push({ + name: op.name, + skipped: true, + reason: 'insufficient_permissions', + required_scopes: op.requiredScopes + }); + continue; + } + } + + // Execute operation + const result = await op.execute(accessToken, results); + results.push({ name: op.name, success: true, data: result }); + } + + return results; +} + +// Example with optional operations +const meetingWithOptionalRecording = [ + { + name: 'getMeeting', + required: true, + requiredScopes: ['meeting:read'], + execute: async (token) => getMeetingDetails(meetingId, token) + }, + { + name: 'getRecordings', + required: false, // Optional - won't fail chain + requiredScopes: ['recording:read'], + execute: async (token, prev) => { + const meeting = prev.find(r => r.name === 'getMeeting').data; + return getRecordings(meeting.uuid, token); + } + } +]; +``` + +## Authorization Decision Flowchart + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ AUTHORIZATION DECISION FLOW │ +└──────────────────────────────────────────────────────────────────────────┘ + + ┌─────────────────┐ + │ Receive Request │ + └────────┬────────┘ + │ + ▼ + ┌────────────────────────┐ + │ Is token present? │ + └───────────┬────────────┘ + │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 401 │ │ Is token valid? │ + │ Unauthorized │ └─────────┬──────────┘ + └───────────────┘ │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 401 │ │ Has required │ + │ Invalid Token │ │ scopes? │ + └───────────────┘ └─────────┬──────────┘ + │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 403 │ │ Has resource │ + │ Insufficient │ │ access? │ + │ Scope │ └─────────┬──────────┘ + └───────────────┘ │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────┐ + │ Return 403 │ │ Execute │ + │ Forbidden │ │ Operation │ + └───────────────┘ └────────────────┘ +``` + +## Common Authorization Errors + +| Status | Error | Cause | Solution | +|--------|-------|-------|----------| +| 401 | `invalid_token` | Token expired or revoked | Refresh token or re-authenticate | +| 401 | `unauthorized` | No token provided | Include Authorization header | +| 403 | `insufficient_scope` | Token missing required scope | Request additional scopes | +| 403 | `forbidden` | User lacks resource access | Check user permissions | +| 403 | `access_denied` | Admin-only operation | Use admin account | + +## Best Practices + +1. **Validate upfront** - Check all permissions before starting a chain +2. **Fail fast** - Return clear error messages with required scopes +3. **Graceful degradation** - Skip optional steps rather than fail entirely +4. **Audit logging** - Log all authorization decisions +5. **Principle of least privilege** - Request only needed scopes +6. **Token caching** - Cache token info to avoid repeated validation calls + +## Real-World Examples + +See these use-cases for authorization patterns in action: + +- **[User + Meeting Creation](../use-cases/user-and-meeting-creation.md)** - Multi-step provisioning with scope validation +- **[Meeting Details with Events](../use-cases/meeting-details-with-events.md)** - REST API + webhooks with permission checking +- **[Meeting Automation](../use-cases/meeting-automation.md)** - Meeting management with admin scope requirements + +## Resources + +- **OAuth Scopes Reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **API Error Codes**: https://developers.zoom.us/docs/api/rest/error-handling/ +- **Authentication Guide**: [authentication.md](authentication.md) +- **Scopes Reference**: [scopes.md](scopes.md) diff --git a/partner-built/zoom-plugin/skills/general/references/automatic-skill-chaining-rest-webhooks.md b/partner-built/zoom-plugin/skills/general/references/automatic-skill-chaining-rest-webhooks.md new file mode 100644 index 00000000..e2f9fc6a --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/automatic-skill-chaining-rest-webhooks.md @@ -0,0 +1,176 @@ +# Automatic Skill Chaining: REST API + Webhooks + +This guide provides executable patterns for handling a multi-faceted workflow that needs both: +- synchronous REST API operations (`zoom-rest-api`) +- asynchronous event processing (`zoom-webhooks`) + +## Chain Selection Logic + +```ts +export type SkillChain = { + selectedSkills: string[]; + executionOrder: string[]; +}; + +export function chooseRestWebhookChain(query: string): SkillChain { + const q = query.toLowerCase(); + const needsRest = /create meeting|update meeting|list users|rest api|\/v2\//.test(q); + const needsWebhook = /webhook|event|meeting\.started|participant|real-time update/.test(q); + + const selectedSkills = ['zoom-general']; + if (needsRest || needsWebhook) selectedSkills.push('zoom-oauth'); + if (needsRest) selectedSkills.push('zoom-rest-api'); + if (needsWebhook) selectedSkills.push('zoom-webhooks'); + + return { + selectedSkills, + executionOrder: selectedSkills, + }; +} +``` + +## Reference Architecture + +```text +Client/API Caller + -> Orchestrator API + -> OAuth token manager + -> REST API worker (create/update meetings) + -> Persistence (meeting state + idempotency keys) + <- immediate REST result + +Zoom Event Pipeline + Zoom -> Webhook ingress (signature verify + URL validation) + -> Queue + -> Event processors + -> State projection / downstream notifications +``` + +## Minimal Runnable Example (Node.js) + +```js +import express from 'express'; +import crypto from 'crypto'; + +const app = express(); +app.use(express.json({ + verify: (req, _res, buf) => { + req.rawBody = buf.toString('utf8'); + }, +})); + +const tokenCache = { accessToken: '', expiresAt: 0 }; +const meetingStore = new Map(); + +async function getAccessToken() { + const now = Date.now(); + if (tokenCache.accessToken && now < tokenCache.expiresAt - 60_000) { + return tokenCache.accessToken; + } + + const params = new URLSearchParams({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID, + }); + + const basic = Buffer.from(`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`).toString('base64'); + const res = await fetch(`https://zoom.us/oauth/token?${params}`, { + method: 'POST', + headers: { Authorization: `Basic ${basic}` }, + }); + + if (!res.ok) throw new Error(`token_exchange_failed:${res.status}`); + const data = await res.json(); + + tokenCache.accessToken = data.access_token; + tokenCache.expiresAt = now + data.expires_in * 1000; + return tokenCache.accessToken; +} + +app.post('/api/meetings', async (req, res) => { + try { + const token = await getAccessToken(); + const hostUserId = process.env.ZOOM_HOST_USER_ID; + if (!hostUserId) { + return res.status(500).json({ error: 'missing_host_user_id', detail: 'Set ZOOM_HOST_USER_ID for S2S meeting creation' }); + } + + const body = { + topic: req.body.topic || 'Auto Meeting', + type: 2, + start_time: req.body.start_time, + duration: req.body.duration || 30, + timezone: req.body.timezone || 'UTC', + }; + + const z = await fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(hostUserId)}/meetings`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await z.json(); + if (!z.ok) return res.status(z.status).json(data); + + meetingStore.set(String(data.id), { status: 'scheduled', topic: data.topic, participants: 0 }); + return res.status(201).json(data); + } catch (err) { + return res.status(500).json({ error: 'create_meeting_failed', detail: String(err) }); + } +}); + +function verifySignature(req) { + const ts = req.headers['x-zm-request-timestamp']; + const sig = req.headers['x-zm-signature']; + const msg = `v0:${ts}:${req.rawBody || ''}`; + const expected = `v0=${crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(msg).digest('hex')}`; + return sig === expected; +} + +app.post('/webhooks/zoom', (req, res) => { + if (req.body.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + if (!verifySignature(req)) return res.status(401).send('invalid_signature'); + + const evt = req.body.event; + const id = String(req.body.payload?.object?.id || ''); + if (id && !meetingStore.has(id)) meetingStore.set(id, { status: 'unknown', participants: 0 }); + + const state = meetingStore.get(id); + if (state) { + if (evt === 'meeting.started') state.status = 'in_progress'; + if (evt === 'meeting.ended') state.status = 'ended'; + if (evt === 'meeting.participant_joined') state.participants += 1; + if (evt === 'meeting.participant_left') state.participants = Math.max(0, state.participants - 1); + } + + return res.status(200).send('ok'); +}); + +app.listen(process.env.PORT || 3001, () => { + console.log('orchestrator listening'); +}); +``` + +## Failure Handling Minimums + +- REST call failures: retry with jitter for `429/5xx`; do not retry `4xx` business errors blindly. +- Webhook ingestion: always return `200` after durable enqueue or local persistence. +- Idempotency: dedupe by `event_id` or (`event`,`event_ts`,`meeting_uuid`) composite key. +- Reconciliation: periodic REST poll to repair missed webhook events. + +## Environment Variables + +- `ZOOM_ACCOUNT_ID` +- `ZOOM_CLIENT_ID` +- `ZOOM_CLIENT_SECRET` +- `ZOOM_HOST_USER_ID` (required for S2S meeting creation; do not rely on `me`) +- `ZOOM_WEBHOOK_SECRET` +- `PORT` diff --git a/partner-built/zoom-plugin/skills/general/references/community-repos.md b/partner-built/zoom-plugin/skills/general/references/community-repos.md new file mode 100644 index 00000000..7499de83 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/community-repos.md @@ -0,0 +1,158 @@ +# Official Zoom Sample Repositories + +Curated list of official repositories from Zoom for development. Organized by product/SDK. + +--- + +## Meeting SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | 643 | Web SDK sample - Component View and Client View | +| [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | 324 | NPM package for embedding meetings | +| [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | 177 | React integration sample | +| [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | 124 | Generate Meeting SDK JWT signatures | +| [meetingsdk-angular-sample](https://github.com/zoom/meetingsdk-angular-sample) | 60 | Angular integration sample | +| [meetingsdk-vuejs-sample](https://github.com/zoom/meetingsdk-vuejs-sample) | 42 | Vue.js integration sample | +| [meetingsdk-javascript-sample](https://github.com/zoom/meetingsdk-javascript-sample) | 41 | Vanilla JavaScript sample | +| [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | 3 | Headless Linux bot with Docker | + +--- + +## Video SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [videosdk-web-sample](https://github.com/zoom/videosdk-web-sample) | 137 | Web Video SDK sample | +| [videosdk-web](https://github.com/zoom/videosdk-web) | 56 | NPM package for custom video | +| [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | 23 | Generate Video SDK JWT signatures | +| [videosdk-zoom-ui-toolkit-web](https://github.com/zoom/videosdk-zoom-ui-toolkit-web) | 17 | Prebuilt video chat UI | +| [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | 17 | UI Toolkit in React | +| [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | 16 | Next.js integration | +| [videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | 11 | UI Toolkit in vanilla JS | +| [VideoSDK-Web-Telehealth](https://github.com/zoom/VideoSDK-Web-Telehealth) | 11 | Telehealth starter kit | +| [videosdk-workshop](https://github.com/zoom/videosdk-workshop) | 9 | Workshop project | +| [videosdk-s3-cloud-recordings](https://github.com/zoom/videosdk-s3-cloud-recordings) | 8 | Auto-upload recordings to S3 | +| [videosdk-web-helloworld](https://github.com/zoom/videosdk-web-helloworld) | 4 | Minimal hello world | +| [videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | 4 | UI Toolkit in Angular | +| [videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | 3 | UI Toolkit in Vue.js | +| [videosdk-vue-nuxt-quickstart](https://github.com/zoom/videosdk-vue-nuxt-quickstart) | 1 | Vue/Nuxt quickstart | +| [videosdk-electron-sample](https://github.com/zoom/videosdk-electron-sample) | 1 | Electron sample | +| [videosdk-linux-raw-recording-sample](https://github.com/zoom/videosdk-linux-raw-recording-sample) | - | Linux headless raw data capture | + +--- + +## REST API + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [oauth-sample-app](https://github.com/zoom/oauth-sample-app) | 91 | Node.js OAuth sample | +| [server-to-server-oauth-starter-api](https://github.com/zoom/server-to-server-oauth-starter-api) | 54 | S2S OAuth starter API | +| [api](https://github.com/zoom/api) | 44 | API v2 documentation | +| [user-level-oauth-starter](https://github.com/zoom/user-level-oauth-starter) | 27 | User-level OAuth starter | +| [server-to-server-oauth-token](https://github.com/zoom/server-to-server-oauth-token) | 15 | S2S token generation utility | +| [rivet-javascript](https://github.com/zoom/rivet-javascript) | 13 | Rivet API library (auth + webhooks + API) | +| [websocket-js-sample](https://github.com/zoom/websocket-js-sample) | 5 | WebSocket connection demo | +| [websocket-redis-example](https://github.com/zoom/websocket-redis-example) | 4 | WebSocket with Redis | +| [server-to-server-python-sample](https://github.com/zoom/server-to-server-python-sample) | 4 | Python S2S OAuth sample | +| [task-manager-sample](https://github.com/zoom/task-manager-sample) | 3 | Unified build flow showcase | +| [rivet-javascript-sample](https://github.com/zoom/rivet-javascript-sample) | 3 | Rivet standup bot sample | +| [sample-registration-app](https://github.com/zoom/sample-registration-app) | 3 | Webinar registration with rate limits | + +--- + +## Webhooks + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [webhook-sample](https://github.com/zoom/webhook-sample) | 34 | Receive Zoom webhooks (Node.js) | +| [zoom-webhook-verification-headers](https://github.com/zoom/zoom-webhook-verification-headers) | - | Custom header auth + webhook validation | +| [webhook-to-postgres](https://github.com/zoom/webhook-to-postgres) | 5 | Store webhooks in PostgreSQL | +| [Go-Webhooks](https://github.com/zoom/Go-Webhooks) | - | Go/Fiber webhook listener | + +--- + +## Zoom Apps SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [zoomapps-sample-js](https://github.com/zoom/zoomapps-sample-js) | 66 | Hello World Zoom App (vanilla JS) | +| [zoomapps-advancedsample-react](https://github.com/zoom/zoomapps-advancedsample-react) | 55 | Advanced React sample | +| [appssdk](https://github.com/zoom/appssdk) | 49 | Zoom Apps SDK NPM package | +| [zoomapps-texteditor-vuejs](https://github.com/zoom/zoomapps-texteditor-vuejs) | 16 | Collaborate Mode text editor | +| [zoomapps-customlayout-js](https://github.com/zoom/zoomapps-customlayout-js) | 16 | Immersive Mode / Layers API | +| [zoomapps-workshop-sample](https://github.com/zoom/zoomapps-workshop-sample) | 6 | Getting started workshop | +| [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) | 6 | Serverless on Firebase | +| [zoomapps-cameramode-vuejs](https://github.com/zoom/zoomapps-cameramode-vuejs) | 6 | Camera Mode + Immersive Mode | +| [arlo-meeting-assistant](https://github.com/zoom/arlo-meeting-assistant) | 2 | RTMS-powered meeting assistant | +| [meetingbot-recall-sample](https://github.com/zoom/meetingbot-recall-sample) | 2 | Meeting bot with Recall.ai + Claude | + +--- + +## RTMS (Real-Time Media Streams) + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [zoom-rtms](https://github.com/zoom/rtms) | 29 | Cross-platform RTMS wrapper (Node.js, Python, Go) | +| [rtms-samples](https://github.com/zoom/rtms-samples) | 22 | Official RTMS sample apps | +| [rtms-developer-preview-js](https://github.com/zoom/rtms-developer-preview-js) | 3 | Developer preview hello world | +| [rtms-sdk-cpp](https://github.com/zoom/rtms-sdk-cpp) | 2 | C++ RTMS SDK (librtmsdk) | +| [rtms-meeting-assistant-starter-kit](https://github.com/zoom/rtms-meeting-assistant-starter-kit) | 1 | Meeting assistant starter kit | +| [rtms-quickstart-js](https://github.com/zoom/rtms-quickstart-js) | 1 | Node.js quickstart | +| [zoom_rtms_langchain_sample](https://github.com/zoom/zoom_rtms_langchain_sample) | 1 | LangChain + transcripts for action items | + +--- + +## Team Chat & Chatbots + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [unsplash-chatbot](https://github.com/zoom/unsplash-chatbot) | 19 | Send Unsplash photos in Team Chat | +| [node.js-chatbot](https://github.com/zoom/node.js-chatbot) | 18 | Node.js chatbot library | +| [vote-chatbot](https://github.com/zoom/vote-chatbot) | 10 | Voting bot for Team Chat | +| [catbot](https://github.com/zoom/catbot) | 9 | Cat photo bot | +| [node.js-chatbot-cli](https://github.com/zoom/node.js-chatbot-cli) | 8 | Chatbot CLI tool | +| [zoom-chatbot-claude-sample](https://github.com/zoom/zoom-chatbot-claude-sample) | 6 | Anthropic Claude in Team Chat | +| [Zoom-Chat-Neural-Search-Assistant-Sample](https://github.com/zoom/Zoom-Chat-Neural-Search-Assistant-Sample) | 2 | Cerebras + Exa search bot | +| [zoom-team-chat-shortcut-sample](https://github.com/zoom/zoom-team-chat-shortcut-sample) | 1 | Recording management shortcut | +| [zoom-teams-chat-snowflake-sample](https://github.com/zoom/zoom-teams-chat-snowflake-sample) | 1 | Snowflake + Cortex integration | +| [zoom-erp-chatbot-sample](https://github.com/zoom/zoom-erp-chatbot-sample) | 1 | Oracle ERP integration | +| [chatbot-nodejs-quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) | - | Node.js chatbot quickstart | +| [chatbot-python-sample](https://github.com/zoom/chatbot-python-sample) | - | Python chatbot with threading | + +--- + +## Cobrowse SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [CobrowseSDK-Quickstart](https://github.com/zoom/CobrowseSDK-Quickstart) | 1 | Cobrowse SDK quickstart | +| [cobrowsesdk-auth-endpoint-sample](https://github.com/zoom/cobrowsesdk-auth-endpoint-sample) | 2 | JWT generation for Cobrowse | + +--- + +## Tooling & Utilities + +### Official Tools (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [probesdk-web](https://github.com/zoom/probesdk-web) | 3 | Test device/network/server connection | + +--- diff --git a/partner-built/zoom-plugin/skills/general/references/distributed-meeting-fallback-architecture.md b/partner-built/zoom-plugin/skills/general/references/distributed-meeting-fallback-architecture.md new file mode 100644 index 00000000..4002ff0e --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/distributed-meeting-fallback-architecture.md @@ -0,0 +1,459 @@ +# Distributed Meeting Creation and Event Processing with Fallbacks + +Use this architecture for high-volume meeting creation with resilient event processing. + +## Core Architectural Considerations + +1. **Separation of planes** +- Command plane: REST meeting creation/update APIs. +- Event plane: webhook ingestion and async projection. + +2. **Idempotency and dedupe** +- Require caller-provided idempotency key per create request. +- Dedupe webhook events by stable event key. + +3. **Token isolation** +- Central token broker with distributed lock (Redis/Postgres advisory lock). + +4. **Backpressure and queueing** +- Queue all webhook events and meeting commands. +- Use DLQ for poison messages. + +5. **Fallback mechanisms** +- Retry with exponential backoff + jitter for retriable failures (`429/5xx/network`). +- Circuit breaker around Zoom API dependency. +- Reconciliation poller when webhook delivery is delayed/missed. + +## Reference Topology + +```text +API Gateway + -> Meeting Command Service + -> Idempotency Store (Redis/Postgres) + -> Token Broker + -> Zoom REST API + -> Outbox/Event Bus + +Webhook Ingress + -> Signature Verify + URL Validation + -> Queue (Kafka/SQS/Rabbit) + -> Projection Workers + -> Meeting State Store + +Recovery Services + -> Retry Worker + -> Reconciliation Poller (REST pull) + -> Dead Letter Reprocessor +``` + +## Command Plane Example (Meeting Creation Service) + +```ts +type CreateMeetingInput = { + idempotencyKey: string; + hostUserId: string; // explicit user for S2S + topic: string; + startTime: string; + duration: number; +}; + +type QueuePublisher = { publish: (topic: string, payload: object) => Promise }; +type IdempotencyStore = { + get: (key: string) => Promise; + put: (key: string, value: object, ttlSec: number) => Promise; +}; + +export async function createMeetingCommand( + input: CreateMeetingInput, + deps: { + tokenBroker: { getToken: () => Promise }; + idempotency: IdempotencyStore; + queue: QueuePublisher; + breaker: CircuitBreaker; + }, +) { + const cached = await deps.idempotency.get(input.idempotencyKey); + if (cached) return cached; + + if (!deps.breaker.canCall()) { + // degraded mode: queue command for delayed processing + await deps.queue.publish('meeting.create.delayed', input); + return { accepted: true, mode: 'degraded_queued' }; + } + + const op = async () => { + const token = await deps.tokenBroker.getToken(); + const res = await fetch( + `https://api.zoom.us/v2/users/${encodeURIComponent(input.hostUserId)}/meetings`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + topic: input.topic, + type: 2, + start_time: input.startTime, + duration: input.duration, + }), + }, + ); + if (!res.ok) { + const err = new Error(`zoom_create_failed:${res.status}`); + (err as any).status = res.status; + throw err; + } + return res.json(); + }; + + try { + const created = await retry( + op, + { retries: 4, baseMs: 300, maxMs: 5000 }, + (e) => [429, 500, 502, 503, 504].includes((e as any).status), + ); + + deps.breaker.recordSuccess(); + await deps.idempotency.put(input.idempotencyKey, created, 3600); + await deps.queue.publish('meeting.created', { meetingId: created.id, hostUserId: input.hostUserId }); + return created; + } catch (e) { + deps.breaker.recordFailure(); + throw e; + } +} +``` + +## Event Plane Example (Webhook Ingress + Queue + Projection) + +```ts +import crypto from 'crypto'; + +export function verifyWebhook(rawBody: string, ts: string, sig: string, secret: string): boolean { + // reject stale requests to reduce replay risk + const nowSec = Math.floor(Date.now() / 1000); + const tsSec = Number(ts || 0); + if (!Number.isFinite(tsSec) || Math.abs(nowSec - tsSec) > 300) return false; + + const msg = `v0:${ts}:${rawBody}`; + const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`; + return sig === expected; +} + +export async function ingestWebhook(req: any, res: any, queue: QueuePublisher, secret: string) { + if (req.body.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + const ts = String(req.headers['x-zm-request-timestamp'] || ''); + const sig = String(req.headers['x-zm-signature'] || ''); + const raw = String(req.rawBody || ''); + if (!verifyWebhook(raw, ts, sig, secret)) return res.status(401).send('invalid_signature'); + + try { + // durable write first, then ack + await queue.publish('zoom.webhook.raw', req.body); + return res.status(200).send('ok'); + } catch { + // non-200 triggers Zoom retry for at-least-once delivery + return res.status(503).send('queue_unavailable'); + } +} + +export async function projectEvent(evt: any, stateStore: any, dedupe: IdempotencyStore) { + const dedupeKey = `${evt.event}:${evt.event_ts}:${evt.payload?.object?.uuid || evt.payload?.object?.id || 'unknown'}`; + const seen = await dedupe.get(dedupeKey); + if (seen) return; + + const id = String(evt.payload?.object?.id || ''); + const current = (await stateStore.get(id)) || { status: 'unknown', participants: 0, lastEventTs: 0 }; + if (evt.event_ts < current.lastEventTs) { + await dedupe.put(dedupeKey, { stale: true }, 86400); + return; + } // stale event guard + + if (evt.event === 'meeting.started') current.status = 'in_progress'; + if (evt.event === 'meeting.ended') current.status = 'ended'; + if (evt.event === 'meeting.participant_joined') current.participants += 1; + if (evt.event === 'meeting.participant_left') current.participants = Math.max(0, current.participants - 1); + current.lastEventTs = evt.event_ts; + + await stateStore.put(id, current); + await dedupe.put(dedupeKey, { ok: true }, 86400); +} +``` + +### Express raw-body setup (required for signature verification) + +```ts +app.use(express.json({ + verify: (req: any, _res, buf) => { + req.rawBody = buf.toString('utf8'); + }, +})); +``` + +## Retry + Circuit Breaker Example (TypeScript) + +```ts +type RetryOptions = { + retries: number; + baseMs: number; + maxMs: number; +}; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +function backoff(attempt: number, baseMs: number, maxMs: number) { + const exp = Math.min(maxMs, baseMs * 2 ** attempt); + const jitter = Math.floor(Math.random() * Math.min(250, exp / 4)); + return exp + jitter; +} + +export async function retry(fn: () => Promise, opts: RetryOptions, isRetriable: (e: any) => boolean): Promise { + let lastErr: any; + for (let i = 0; i <= opts.retries; i += 1) { + try { + return await fn(); + } catch (e) { + lastErr = e; + if (i === opts.retries || !isRetriable(e)) break; + await sleep(backoff(i, opts.baseMs, opts.maxMs)); + } + } + throw lastErr; +} + +export class CircuitBreaker { + private failures = 0; + private openUntil = 0; + + constructor(private threshold = 5, private coolDownMs = 15_000) {} + + canCall() { + return Date.now() > this.openUntil; + } + + recordSuccess() { + this.failures = 0; + } + + recordFailure() { + this.failures += 1; + if (this.failures >= this.threshold) { + this.openUntil = Date.now() + this.coolDownMs; + } + } +} +``` + +## Reconciliation Poller Example (Fallback for Missed Events) + +```ts +export async function reconcileMeetingState( + meetingId: string, + hostUserId: string, + deps: { + tokenBroker: { getToken: () => Promise }; + stateStore: { get: (id: string) => Promise; put: (id: string, v: any) => Promise }; + }, +) { + const token = await deps.tokenBroker.getToken(); + const res = await fetch(`https://api.zoom.us/v2/meetings/${encodeURIComponent(meetingId)}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return; + + const apiState = await res.json(); + const projected = (await deps.stateStore.get(meetingId)) || {}; + const merged = { + ...projected, + status: apiState.status || projected.status, + topic: apiState.topic || projected.topic, + hostId: hostUserId, + reconciledAt: Date.now(), + }; + await deps.stateStore.put(meetingId, merged); +} +``` + +## Distributed Coordination and Load-Balancing Considerations + +- Partition command/event streams by `meetingId` or `hostUserId` so all updates for one meeting land on the same consumer shard. +- Use a distributed lock for shared singleton jobs (token refresh rotation, reconciliation scheduler leader). +- Keep webhook ingress stateless so horizontal autoscaling is safe behind L4/L7 load balancers. +- Apply queue consumer concurrency limits to protect downstream Zoom API quotas. + +### Redis-Style Lock Skeleton + +```ts +export async function withLock(lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }, key: string, fn: () => Promise) { + const got = await lock.acquire(key, 10_000); + if (!got) return; + try { + await fn(); + } finally { + await lock.release(key); + } +} +``` + +## Token Broker Example (Cached Refresh + Distributed Lock) + +```ts +type CachedToken = { accessToken: string; expiresAtMs: number }; + +export class TokenBroker { + constructor( + private cache: { get: (k: string) => Promise; put: (k: string, v: CachedToken, ttlSec: number) => Promise }, + private lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }, + private fetchToken: () => Promise<{ access_token: string; expires_in: number }>, + ) {} + + async getToken(): Promise { + const cached = await this.cache.get('zoom:s2s-token'); + const now = Date.now(); + if (cached && cached.expiresAtMs - now > 60_000) { + return cached.accessToken; + } + + const gotLock = await this.lock.acquire('zoom:s2s-token:refresh', 10_000); + if (!gotLock) { + await sleep(200); + const retryCached = await this.cache.get('zoom:s2s-token'); + if (retryCached && retryCached.expiresAtMs - Date.now() > 30_000) { + return retryCached.accessToken; + } + throw new Error('token_refresh_lock_contention'); + } + + try { + const fresh = await this.fetchToken(); + const value = { + accessToken: fresh.access_token, + expiresAtMs: Date.now() + fresh.expires_in * 1000, + }; + await this.cache.put('zoom:s2s-token', value, Math.max(60, fresh.expires_in - 90)); + return value.accessToken; + } finally { + await this.lock.release('zoom:s2s-token:refresh'); + } + } +} +``` + +## High-Volume Create Worker (Concurrency + Rate Protection) + +```ts +type CreateJob = CreateMeetingInput & { attempts: number }; + +class TokenBucket { + private tokens: number; + private lastRefill = Date.now(); + + constructor(private readonly capacity: number, private readonly refillPerSec: number) { + this.tokens = capacity; + } + + async take() { + while (true) { + const now = Date.now(); + const elapsedSec = (now - this.lastRefill) / 1000; + this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec); + this.lastRefill = now; + if (this.tokens >= 1) { + this.tokens -= 1; + return; + } + await sleep(100); + } + } +} + +export async function runCreateWorker( + queue: { receiveBatch: (n: number) => Promise; ack: (job: CreateJob) => Promise; retryLater: (job: CreateJob, delayMs: number) => Promise }, + deps: { + createMeeting: (job: CreateJob) => Promise; + breaker: CircuitBreaker; + limiter: TokenBucket; + }, + concurrency = 8, +) { + while (true) { + const jobs = await queue.receiveBatch(concurrency); + await Promise.all(jobs.map(async (job) => { + if (!deps.breaker.canCall()) { + await queue.retryLater(job, 30_000); + return; + } + + try { + await deps.limiter.take(); + await deps.createMeeting(job); + deps.breaker.recordSuccess(); + await queue.ack(job); + } catch (e: any) { + deps.breaker.recordFailure(); + const delay = backoff(job.attempts, 500, 60_000); + await queue.retryLater({ ...job, attempts: job.attempts + 1 }, delay); + } + })); + } +} +``` + +## Reconciliation Scheduler (Lag Detection + Leader Election) + +```ts +export async function reconcileLaggingMeetings( + deps: { + lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }; + stateStore: { listLagging: (ageMs: number, limit: number) => Promise> }; + reconcile: (meetingId: string, hostUserId: string) => Promise; + }, +) { + await withLock(deps.lock, 'zoom:reconcile:leader', async () => { + const lagging = await deps.stateStore.listLagging(5 * 60_000, 250); + for (const item of lagging) { + await deps.reconcile(item.meetingId, item.hostUserId); + } + }); +} +``` + +## DLQ Replay Worker + +```ts +export async function replayDlq( + dlq: { receiveBatch: (n: number) => Promise; ack: (msg: any) => Promise; moveBack: (topic: string, msg: any) => Promise }, + topic = 'meeting.create.delayed', +) { + const failed = await dlq.receiveBatch(100); + for (const msg of failed) { + await dlq.moveBack(topic, { ...msg, replayedAt: Date.now() }); + await dlq.ack(msg); + } +} +``` + +## Distributed State Rules + +- Meeting state is event-sourced or projection-based, not only request-response based. +- Persist `last_seen_event_ts` and status transitions to handle out-of-order events. +- Add monotonic transition guards (e.g., do not move `ended -> in_progress`). + +## Fallback Matrix + +| Failure | Primary response | Fallback | +|---|---|---| +| Token refresh failure | Retry token exchange | Fail fast + alert + pause new create requests | +| REST `429` / `5xx` | Retry w/ backoff | Queue command for delayed retry | +| Webhook verification failure | Reject `401` | Alert security pipeline | +| Webhook processor down | Buffer in queue | DLQ + replay job | +| Missing webhook event | Detect via reconciliation lag | REST poll and repair projection | +| Dependency outage | Open circuit breaker | Serve degraded status + queued commands | diff --git a/partner-built/zoom-plugin/skills/general/references/environment-variables.md b/partner-built/zoom-plugin/skills/general/references/environment-variables.md new file mode 100644 index 00000000..b48dc794 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/environment-variables.md @@ -0,0 +1,38 @@ +# Cross-Product Environment Variables (Hub) + +Use this file as a normalization map. Product-specific details are maintained in each product skill reference. + +## Common `.env` keys + +| Variable | Typical products | Where to find | +| --- | --- | --- | +| `ZOOM_CLIENT_ID` | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials | +| `ZOOM_ACCOUNT_ID` | Server-to-Server OAuth flows | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_REDIRECT_URI` | User-level OAuth apps | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_WEBHOOK_SECRET` / `WEBHOOK_SECRET_TOKEN` | Webhooks and event validation | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `ZOOM_SDK_KEY` / `ZOOM_SDK_SECRET` | Meeting SDK or SDK-based products | Zoom Marketplace -> SDK app credentials | +| `ZOOM_VIDEO_SDK_KEY` / `ZOOM_VIDEO_SDK_SECRET` | Video SDK and UI Toolkit | Zoom Marketplace -> Video SDK app credentials | +| `PROBE_JS_URL` / `PROBE_WASM_URL` | Probe SDK | Your app/CDN hosted Probe SDK assets (or bundler output paths) | +| `PROBE_DOMAIN` / `PROBE_CONNECT_TIMEOUT_MS` | Probe SDK | Product policy + Probe SDK diagnostics configuration | + +## Product references + +- [../../zoom-apps-sdk/references/environment-variables.md](../../zoom-apps-sdk/references/environment-variables.md) +- [../../cobrowse-sdk/references/environment-variables.md](../../cobrowse-sdk/references/environment-variables.md) +- [../../meeting-sdk/references/environment-variables.md](../../meeting-sdk/references/environment-variables.md) +- [../../oauth/references/environment-variables.md](../../oauth/references/environment-variables.md) +- [../../rest-api/references/environment-variables.md](../../rest-api/references/environment-variables.md) +- [../../rtms/references/environment-variables.md](../../rtms/references/environment-variables.md) +- [../../team-chat/references/environment-variables.md](../../team-chat/references/environment-variables.md) +- [../../ui-toolkit/references/environment-variables.md](../../ui-toolkit/references/environment-variables.md) +- [../../video-sdk/references/environment-variables.md](../../video-sdk/references/environment-variables.md) +- [../../webhooks/references/environment-variables.md](../../webhooks/references/environment-variables.md) +- [../../websockets/references/environment-variables.md](../../websockets/references/environment-variables.md) +- [../../contact-center/references/environment-variables.md](../../contact-center/references/environment-variables.md) +- [../../phone/references/environment-variables.md](../../phone/references/environment-variables.md) +- [../../probe-sdk/references/environment-variables.md](../../probe-sdk/references/environment-variables.md) + +## Probe SDK note + +- Probe SDK core diagnostics do not require Zoom OAuth/Marketplace credentials. diff --git a/partner-built/zoom-plugin/skills/general/references/interview-answer-routing.md b/partner-built/zoom-plugin/skills/general/references/interview-answer-routing.md new file mode 100644 index 00000000..715d2ca5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/interview-answer-routing.md @@ -0,0 +1,20 @@ +# Interview Answer: Routing with zoom-general + +Use `zoom-general` as the triage layer, then route implementation to specialized skills. + +## Short answer + +1. Classify the query in `zoom-general` by product intent, platform, and integration pattern. +2. Route to the minimum specialized skills: +- Auth/scopes -> `zoom-oauth` +- API operations -> `zoom-rest-api` +- Embedded meetings -> `zoom-meeting-sdk` +- Custom video experiences -> `zoom-video-sdk` +- Event delivery -> `zoom-webhooks` or `zoom-websockets` +- Live media/transcripts -> `zoom-rtms` +3. Execute in sequence: `zoom-general` -> auth -> core product -> events/media. +4. If ambiguous, ask one disambiguation question before locking the chain. + +Canonical guidance and handoff structure: +- [Query Routing Playbook](query-routing-playbook.md) + diff --git a/partner-built/zoom-plugin/skills/general/references/known-limitations.md b/partner-built/zoom-plugin/skills/general/references/known-limitations.md new file mode 100644 index 00000000..3e79a594 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/known-limitations.md @@ -0,0 +1,101 @@ +# Known Limitations & Quirks + +Common gotchas and limitations developers encounter. + +## Recording Limitations + +### Minimum Recording Duration + +**Recordings shorter than 3-5 seconds will NOT be saved.** + +This applies to: +- Cloud recordings +- Local recordings via SDK + +If you need to capture very short sessions, ensure the recording runs for at least 5 seconds. + +## API Limitations + +### Rate Limits + +See [Rate Limits](../../rest-api/references/rate-limits.md) for detailed information. + +Key points: +- Create/update meeting endpoints are **Heavy** (stricter limits) +- Response headers show remaining quota +- Implement exponential backoff for 429 errors + +### Error Code 0 + +**The enum value 0 often represents SUCCESS, not failure.** + +Always check the SDK enum values: +```cpp +// Example: Meeting SDK +SDKERR_SUCCESS = 0 // This is success! +SDKERR_UNKNOWN = 1 // This is an error +``` + +Don't assume 0 = error in your error handling. + +## Video SDK Web Limitations + +### Video Rendering Performance + +**Use ONE rendering control for all videos, not one per participant.** + +Multiple rendering controls severely degrade performance. See [Video SDK Web](../../video-sdk/web/references/web.md#video-rendering-best-practices). + +### SharedArrayBuffer + +Some features require SharedArrayBuffer headers: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +As of v1.11.2, this is elective for basic functionality. + +## SDK Signature Limitations + +### Minimum Token Validity + +Zoom may require `exp - iat >= 2 hours`. + +**Workaround:** Set `iat` in the past: +```javascript +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now +``` + +This gives you a short-lived token while satisfying the validity requirement. + +## SDK Download + +### Marketplace Sign-in Required + +Meeting SDK and Video SDK (except Web npm packages) must be downloaded from [Marketplace](https://marketplace.zoom.us/) after signing in. + +They are not available on public package managers for native platforms. + +## Platform-Specific + +### iOS + +- Requires camera/microphone entitlements +- Background audio requires special configuration + +### Android + +- Requires runtime permissions for camera/mic +- ProGuard rules may be needed + +### Linux + +- Headless operation requires X virtual framebuffer (Xvfb) for some features +- Limited UI customization compared to other platforms + +## Resources + +- **Developer forum**: https://devforum.zoom.us/ (search for known issues) +- **Support**: https://devsupport.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/general/references/marketplace.md b/partner-built/zoom-plugin/skills/general/references/marketplace.md new file mode 100644 index 00000000..7d634683 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/marketplace.md @@ -0,0 +1,67 @@ +# Zoom App Marketplace + +Navigate the Zoom Marketplace developer portal. + +## Overview + +The [Zoom App Marketplace](https://marketplace.zoom.us/) is where you create, configure, and publish Zoom apps. + +## Getting Started + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us/) +2. Sign in with your Zoom account +3. Click **Develop** → **Build App** +4. Choose app type +5. Configure app settings + +## Portal Sections + +### Develop + +- **Build App** - Create new apps +- **Manage** - Edit existing apps +- **Logs** - View API and webhook logs + +### App Configuration + +| Section | Purpose | +|---------|---------| +| **App Credentials** | SDK Key/Secret, Client ID/Secret | +| **Scopes** | Configure OAuth permissions | +| **Feature** | Enable Meeting SDK, Video SDK, Webhooks | +| **Activation** | Make app installable | + +## SDK Downloads + +**Important:** Meeting SDK and Video SDK must be downloaded from Marketplace after signing in. They are not available on public package managers (except Web SDKs via npm). + +1. Go to your app's **Download** section +2. Select platform (iOS, Android, Windows, macOS, Linux) +3. Download SDK package + +## Credentials + +### OAuth Apps + +- **Client ID** - Public identifier +- **Client Secret** - Keep secret, server-side only + +### SDK Apps + +- **SDK Key** - Used in JWT payload +- **SDK Secret** - Used to sign JWT, keep secret + +## Publishing + +To publish to Marketplace: + +1. Complete app configuration +2. Submit for review +3. Address feedback +4. Get approved +5. Go live + +## Resources + +- **Marketplace**: https://marketplace.zoom.us/ +- **Developer docs**: https://developers.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md b/partner-built/zoom-plugin/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md new file mode 100644 index 00000000..588cf71a --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md @@ -0,0 +1,173 @@ +# Meeting + Webhooks + OAuth Refresh Orchestration + +This guide implements one solution that handles all three simultaneously: +1. create meeting, +2. process webhook updates, +3. refresh OAuth tokens safely. + +## Direct Answer + +Use this skill chain: + +1. `zoom-general` to classify the request +2. `zoom-oauth` for token brokerage and refresh control +3. `zoom-rest-api` to create the meeting +4. `zoom-webhooks` to receive real-time updates + +Minimal flow: + +```text +client request + -> TokenBroker.getToken() + -> POST /v2/users/{userId}/meetings + -> persist meeting + idempotency key + -> Zoom sends webhooks to your ingress + -> verify signature + -> enqueue event + -> projection worker updates meeting state +``` + +Webhook subscription note: +- the receiver implementation lives in your app code +- the actual Zoom event subscription is configured at the Marketplace app level +- do not model webhook subscription enablement as a per-request runtime API step unless Zoom exposes a product-specific admin API for that exact surface + +## Skill Chain + +1. `zoom-general` +2. `zoom-oauth` +3. `zoom-rest-api` +4. `zoom-webhooks` + +## Component Design + +- `TokenBroker`: central access token cache + refresh lock. +- `MeetingService`: REST calls using broker. +- `WebhookIngress`: signature validation + URL validation + event enqueue. +- `ProjectionWorker`: applies events to meeting state. + +## Token Broker with Refresh Lock (TypeScript) + +```ts +type TokenState = { accessToken: string; expiresAt: number; refreshing?: Promise }; + +export class TokenBroker { + private state: TokenState = { accessToken: '', expiresAt: 0 }; + + constructor( + private accountId: string, + private clientId: string, + private clientSecret: string, + ) {} + + async getToken(): Promise { + const now = Date.now(); + if (this.state.accessToken && now < this.state.expiresAt - 60_000) { + return this.state.accessToken; + } + + if (!this.state.refreshing) { + this.state.refreshing = this.refresh(); + this.state.refreshing.finally(() => { this.state.refreshing = undefined; }); + } + + return this.state.refreshing; + } + + invalidate() { + this.state.accessToken = ''; + this.state.expiresAt = 0; + } + + async forceRefresh(): Promise { + this.invalidate(); + return this.getToken(); + } + + private async refresh(): Promise { + const q = new URLSearchParams({ grant_type: 'account_credentials', account_id: this.accountId }); + const basic = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); + + const res = await fetch(`https://zoom.us/oauth/token?${q.toString()}`, { + method: 'POST', + headers: { Authorization: `Basic ${basic}` }, + }); + + if (!res.ok) throw new Error(`token_refresh_failed:${res.status}`); + const data = await res.json() as { access_token: string; expires_in: number }; + + this.state.accessToken = data.access_token; + this.state.expiresAt = Date.now() + data.expires_in * 1000; + return this.state.accessToken; + } +} +``` + +## Meeting Service with 401 Retry-once + +```ts +export async function createMeeting(tokenBroker: TokenBroker, userId: string, payload: object) { + async function call(): Promise { + const token = await tokenBroker.getToken(); + return fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(userId)}/meetings`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } + + let res = await call(); + if (res.status === 401) { + await tokenBroker.forceRefresh(); + res = await call(); // retry once with fresh token + } + + if (!res.ok) throw new Error(`create_meeting_failed:${res.status}`); + return res.json(); +} +``` + +## Webhook Ingress Skeleton + +```ts +import crypto from 'crypto'; +import type { Request, Response } from 'express'; + +export function verifyZoomSignature(req: Request, secret: string): boolean { + const ts = String(req.headers['x-zm-request-timestamp'] || ''); + const sig = String(req.headers['x-zm-signature'] || ''); + const rawBody = (req as any).rawBody || JSON.stringify(req.body); + const msg = `v0:${ts}:${rawBody}`; + const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`; + return sig === expected; +} + +export async function handleWebhook(req: Request, res: Response, secret: string, enqueue: (e: any) => Promise) { + if (req.body?.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + if (!verifyZoomSignature(req, secret)) { + return res.status(401).send('invalid_signature'); + } + + await enqueue(req.body); // durable queue write + return res.status(200).send('ok'); +} +``` + +## Event Processing Rules + +- Apply idempotency key to avoid duplicate state updates. +- Accept out-of-order events; keep `last_event_ts` and reject stale writes when necessary. +- Add reconciliation worker that polls REST meeting status if webhook lag or failures are detected. + +## Runtime Setup Notes + +- For Server-to-Server OAuth meeting creation, pass an explicit host `userId`/email instead of relying on `me`. +- In Express, capture raw request body in `express.json({ verify })` and use it for signature verification. diff --git a/partner-built/zoom-plugin/skills/general/references/query-routing-playbook.md b/partner-built/zoom-plugin/skills/general/references/query-routing-playbook.md new file mode 100644 index 00000000..2c40e836 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/query-routing-playbook.md @@ -0,0 +1,87 @@ +# Query Routing Playbook (zoom-general) + +Use `zoom-general` as the routing/orchestration layer. +Do not implement product-specific logic in `zoom-general` if a specialized skill exists. + +## Goal + +Convert a complex developer query into: +- `selected_skills` +- `execution_order` +- `assumptions` +- `next_actions` + +## Routing rules + +| Query signal | Route to skill | Why | +|---|---|---| +| OAuth, scopes, S2S, token strategy | `zoom-oauth` | Authentication and authorization design | +| Meetings/users/recordings/reports API operations | `zoom-rest-api` | Server-side Zoom resource management | +| Embed full Zoom meetings/webinars | `zoom-meeting-sdk` | Meeting runtime integration | +| Build custom video session experience | `zoom-video-sdk` | Custom media UX runtime | +| Receive event callbacks via HTTP | `zoom-webhooks` | Event lifecycle notifications | +| Need lower-latency event stream | `zoom-websockets` | Persistent real-time event transport | +| Live audio/video/transcript stream ingestion | `zoom-rtms` | Real-time media and transcript pipeline | +| App runs inside Zoom client | `zoom-apps-sdk` | In-client app model and APIs | + +## Sequencing + +1. Start with `zoom-general` (triage and architecture). +2. Add `zoom-oauth` if any protected resource access is required. +3. Select one primary runtime/API skill (`zoom-meeting-sdk`, `zoom-video-sdk`, or `zoom-rest-api`). +4. Add event/media skills (`zoom-webhooks`, `zoom-websockets`, `zoom-rtms`) based on requirements. +5. Keep the chain minimal; do not add extra skills without explicit need. + +## Handoff contract + +```json +{ + "selected_skills": [ + "zoom-general", + "zoom-oauth", + "zoom-meeting-sdk", + "zoom-webhooks" + ], + "execution_order": [ + "zoom-general", + "zoom-oauth", + "zoom-meeting-sdk", + "zoom-webhooks" + ], + "assumptions": [ + "embedded meeting experience required", + "server-side event endpoint available" + ], + "next_actions": [ + "confirm OAuth scopes", + "implement auth/token flow", + "implement runtime integration", + "implement event consumer and verification" + ] +} +``` + +## Ambiguity handling + +If confidence is low, ask one focused question before final routing: +- “Do you need embedded Zoom meetings, or a fully custom video session UI?” +- “Is webhook latency acceptable, or do you require persistent low-latency events?” + +## Example route + +Query: “Build a Linux bot that joins meetings, auto-creates meetings, streams transcript, and tracks lifecycle events.” + +Recommended chain: +- `zoom-general` +- `zoom-oauth` +- `zoom-rest-api` +- `zoom-meeting-sdk` +- `zoom-rtms` +- `zoom-webhooks` + +Why: +- `zoom-rest-api` for meeting provisioning +- `zoom-meeting-sdk` for runtime join/control +- `zoom-rtms` for live transcript/media stream +- `zoom-webhooks` for lifecycle notifications + diff --git a/partner-built/zoom-plugin/skills/general/references/routing-implementation.md b/partner-built/zoom-plugin/skills/general/references/routing-implementation.md new file mode 100644 index 00000000..c73240f7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/routing-implementation.md @@ -0,0 +1,247 @@ +# Routing Implementation (zoom-general) + +This reference provides a concrete implementation model for routing a complex developer query from `zoom-general` to specialized skills. + +## Runtime Assumptions + +- Runtime: Node.js 18+. +- Language: TypeScript 5+. +- Input: free-form developer prompt. +- Output: deterministic handoff contract with primary skill, chained skills, rationale, and follow-up questions (if required). + +## TypeScript Router Example + +```ts +export type SkillId = + | 'zoom-general' + | 'zoom-rest-api' + | 'zoom-mcp' + | 'zoom-mcp/whiteboard' + | 'zoom-webhooks' + | 'zoom-websockets' + | 'zoom-meeting-sdk' + | 'zoom-meeting-sdk-web' + | 'zoom-meeting-sdk-web-component-view' + | 'zoom-video-sdk' + | 'zoom-video-sdk-web' + | 'zoom-apps-sdk' + | 'zoom-rtms' + | 'zoom-team-chat' + | 'contact-center' + | 'virtual-agent' + | 'phone' + | 'rivet-sdk' + | 'probe-sdk' + | 'zoom-ui-toolkit' + | 'zoom-cobrowse-sdk' + | 'zoom-oauth'; + +export interface RouteDecision { + primarySkill: SkillId; + chainedSkills: SkillId[]; + confidence: number; + rationale: string[]; + needsClarification: string[]; + warnings: string[]; +} + +interface Signals { + meetingEmbed: boolean; + meetingCustomUi: boolean; + customVideo: boolean; + restApi: boolean; + mcp: boolean; + whiteboardMcp: boolean; + webhooks: boolean; + websockets: boolean; + zoomApps: boolean; + oauth: boolean; + rtms: boolean; + teamChat: boolean; + contactCenter: boolean; + virtualAgent: boolean; + phone: boolean; + rivet: boolean; + preflight: boolean; + uiToolkit: boolean; + cobrowse: boolean; +} + +const hasAny = (q: string, words: string[]): boolean => words.some((w) => q.includes(w)); + +export function detectSignals(rawQuery: string): Signals { + const q = rawQuery.toLowerCase(); + return { + meetingEmbed: hasAny(q, ['meeting sdk', 'embed meeting', 'join meeting ui', 'client view', 'component view']), + meetingCustomUi: hasAny(q, [ + 'custom meeting ui', + 'custom zoom meeting ui', + 'custom meeting video ui', + 'custom video ui for meeting', + 'zoommtgembedded', + 'zoomapproot', + 'embeddable meeting ui', + 'component view', + ]), + customVideo: hasAny(q, ['video sdk', 'custom video', 'attachvideo', 'peer-video-state-change']), + restApi: hasAny(q, ['rest api', 'api create meeting', 'api list meetings', '/v2/', 'list users', 's2s oauth', 'meeting endpoint']), + mcp: hasAny(q, ['zoom mcp', 'mcp server', 'agentic retrieval', 'tools/list', 'tools/call', 'semantic meeting search']), + whiteboardMcp: hasAny(q, ['whiteboard mcp', 'zoom whiteboard mcp', 'list whiteboards', 'get a whiteboard', 'wb/db', 'whiteboard_id']), + webhooks: hasAny(q, ['webhook', 'x-zm-signature', 'event subscription', 'crc']), + websockets: hasAny(q, ['websocket', 'real-time events', 'persistent connection']), + zoomApps: hasAny(q, ['zoom apps sdk', 'in-client app', 'layers api', 'collaborate mode']), + oauth: hasAny(q, ['oauth', 'pkce', 'authorization code', 'account_credentials', 'token refresh']), + rtms: hasAny(q, ['rtms', 'real-time media streams', 'live transcript stream', 'audio stream']), + teamChat: hasAny(q, ['team chat', 'chatbot', 'chat card', 'chat message']), + contactCenter: hasAny(q, ['contact center', 'engagement context', 'contact center smart embed', 'zcc']), + virtualAgent: hasAny(q, ['virtual agent', 'zva', 'knowledge base sync', 'virtual assistant sdk']), + phone: hasAny(q, ['zoom phone', 'phone smart embed', 'phone api', 'click to dial']), + rivet: hasAny(q, ['rivet', 'zoom rivet']), + preflight: hasAny(q, ['probe sdk', 'preflight', 'diagnostics', 'network readiness']), + uiToolkit: hasAny(q, ['ui toolkit', 'prebuilt video ui']), + cobrowse: hasAny(q, ['cobrowse', 'co-browse', 'shared browsing']), + }; +} + +function pickPrimarySkill(s: Signals): SkillId { + // Hard guardrails: SDK embed/custom-video requests should not fall back to REST. + if (s.meetingCustomUi) return 'zoom-meeting-sdk-web-component-view'; + if (s.meetingEmbed && !s.customVideo) return 'zoom-meeting-sdk-web'; + if (s.meetingEmbed) return 'zoom-meeting-sdk'; + if (s.customVideo && !s.meetingEmbed) return 'zoom-video-sdk-web'; + if (s.customVideo) return 'zoom-video-sdk'; + + if (s.virtualAgent) return 'virtual-agent'; + if (s.contactCenter) return 'contact-center'; + if (s.zoomApps) return 'zoom-apps-sdk'; + if (s.rtms) return 'zoom-rtms'; + if (s.teamChat) return 'zoom-team-chat'; + if (s.phone) return 'phone'; + if (s.cobrowse) return 'zoom-cobrowse-sdk'; + if (s.uiToolkit) return 'zoom-ui-toolkit'; + if (s.preflight) return 'probe-sdk'; + if (s.websockets) return 'zoom-websockets'; + if (s.webhooks) return 'zoom-webhooks'; + if (s.whiteboardMcp) return 'zoom-mcp/whiteboard'; + if (s.mcp) return 'zoom-mcp'; + if (s.restApi) return 'zoom-rest-api'; + if (s.oauth) return 'zoom-oauth'; + + return 'zoom-general'; +} + +function buildChain(primary: SkillId, s: Signals): SkillId[] { + const chain = new Set(); + + if (primary === 'zoom-meeting-sdk-web-component-view') chain.add('zoom-meeting-sdk-web'); + + // Auth chaining. + if (s.oauth || s.restApi || s.mcp || s.webhooks || s.websockets || s.phone || s.teamChat || s.virtualAgent) { + chain.add('zoom-oauth'); + } + + // Optional server framework. + if (s.rivet) chain.add('rivet-sdk'); + + // Cross-surface chaining. + if (primary === 'contact-center' && s.virtualAgent) chain.add('virtual-agent'); + if (primary === 'virtual-agent' && s.contactCenter) chain.add('contact-center'); + + // Event channels often pair with REST resource management. + if (s.webhooks || s.websockets) chain.add('zoom-rest-api'); + if (s.mcp && s.restApi) { + chain.add('zoom-rest-api'); + chain.add('zoom-mcp'); + } + + // Avoid redundant primary in chain. + chain.delete(primary); + + return [...chain]; +} + +function validateDecision(primary: SkillId, s: Signals): string[] { + const warnings: string[] = []; + + if (s.meetingEmbed && !['zoom-meeting-sdk', 'zoom-meeting-sdk-web', 'zoom-meeting-sdk-web-component-view'].includes(primary)) { + warnings.push('meeting embed intent detected but primary skill is not zoom-meeting-sdk'); + } + if (s.meetingCustomUi && primary !== 'zoom-meeting-sdk-web-component-view') { + warnings.push('custom meeting UI intent detected but primary skill is not zoom-meeting-sdk-web-component-view'); + } + if (s.customVideo && !['zoom-video-sdk', 'zoom-video-sdk-web'].includes(primary)) { + warnings.push('custom video intent detected but primary skill is not zoom-video-sdk'); + } + if (s.meetingCustomUi && s.customVideo) { + warnings.push('meeting UI intent and custom video intent both detected; prefer Meeting SDK Component View unless the user explicitly wants a non-meeting session'); + } + if (s.restApi && (s.meetingEmbed || s.customVideo)) { + warnings.push('mixed SDK + REST intent; keep SDK as primary and use REST only for resource workflows'); + } + + return warnings; +} + +function confidenceFromSignals(s: Signals): number { + const hits = Object.values(s).filter(Boolean).length; + if (hits >= 4) return 0.9; + if (hits >= 2) return 0.78; + if (hits === 1) return 0.65; + return 0.5; +} + +export function routeComplexQuery(query: string): RouteDecision { + const signals = detectSignals(query); + const primarySkill = pickPrimarySkill(signals); + const chainedSkills = buildChain(primarySkill, signals); + const warnings = validateDecision(primarySkill, signals); + + const needsClarification: string[] = []; + if (signals.mcp && signals.restApi) { + needsClarification.push('Do you want deterministic REST API automation, AI-agent MCP tooling, or a hybrid of both?'); + } + if (primarySkill === 'zoom-general') { + needsClarification.push('Do you need SDK embed behavior, API resource automation, or event ingestion?'); + } + + const rationale = [ + `primary=${primarySkill}`, + `signals=${JSON.stringify(signals)}`, + `chained=${chainedSkills.join(',') || 'none'}`, + ]; + + return { + primarySkill, + chainedSkills, + confidence: confidenceFromSignals(signals), + rationale, + needsClarification, + warnings, + }; +} +``` + +## Handoff Contract (Example Output) + +```json +{ + "primarySkill": "zoom-meeting-sdk", + "chainedSkills": ["zoom-oauth", "zoom-rest-api", "zoom-webhooks"], + "confidence": 0.9, + "rationale": [ + "primary=zoom-meeting-sdk", + "signals={\"meetingEmbed\":true,\"restApi\":true,\"webhooks\":true,...}", + "chained=zoom-oauth,zoom-rest-api,zoom-webhooks" + ], + "needsClarification": [], + "warnings": [ + "mixed SDK + REST intent; keep SDK as primary and use REST only for resource workflows" + ] +} +``` + +## Error Handling Expectations + +- Unknown/low-signal prompts route to `zoom-general` with one clarifying question. +- Conflicting signals do not fail hard; produce warnings and preserve guardrails. +- Routing should be deterministic for the same normalized prompt. diff --git a/partner-built/zoom-plugin/skills/general/references/scopes.md b/partner-built/zoom-plugin/skills/general/references/scopes.md new file mode 100644 index 00000000..f5ebdc56 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/scopes.md @@ -0,0 +1,94 @@ +# OAuth Scopes + +OAuth scopes define what your app can access. + +## Overview + +Scopes are permissions requested during OAuth authorization. Request only the scopes you need. + +## IMPORTANT: Scope Types + +**Different OAuth types have different scopes available:** + +| OAuth Type | Scope Suffix | Access Level | Example | +|------------|--------------|--------------|---------| +| **User OAuth** | (none) | Current user's data only | `meeting:read` | +| **Admin OAuth** | `:admin` | All users in account | `meeting:read:admin` | +| **Server-to-Server (S2S)** | `:admin` | All users in account (no user consent) | `meeting:read:admin` | + +### Key Differences + +- **User scopes** (`meeting:read`): Access only the authenticated user's data +- **Admin scopes** (`meeting:read:admin`): Access data for ALL users in the account +- **S2S OAuth**: Uses admin-level scopes but doesn't require user login - intended for backend integrations + +### Choosing the Right Scope Type + +| Use Case | OAuth Type | Scope Example | +|----------|------------|---------------| +| User manages their own meetings | User OAuth | `meeting:write` | +| Admin dashboard for all users | Admin OAuth | `meeting:read:admin` | +| Backend automation (no user login) | Server-to-Server | `meeting:write:admin` | +| Bot that creates meetings for users | Server-to-Server | `meeting:write:admin` | + +## Common Scopes + +### Meetings + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `meeting:read` | `meeting:read:admin` | View meeting details | +| `meeting:write` | `meeting:write:admin` | Create, update, delete meetings | +| `meeting:master` | `meeting:master:admin` | Full meeting access | + +### Users + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `user:read` | `user:read:admin` | View user profile | +| `user:write` | `user:write:admin` | Update user settings | +| `user:master` | `user:master:admin` | Full user access | + +### Recordings + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `recording:read` | `recording:read:admin` | View/download recordings | +| `recording:write` | `recording:write:admin` | Delete recordings | +| `recording:master` | `recording:master:admin` | Full recording access | + +### Webinars + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `webinar:read` | `webinar:read:admin` | View webinar details | +| `webinar:write` | `webinar:write:admin` | Create, update webinars | +| `webinar:master` | `webinar:master:admin` | Full webinar access | + +### Reports + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `report:read` | `report:read:admin` | View reports and analytics | +| `report:master` | `report:master:admin` | Full report access | + +## Scope Patterns + +| Pattern | Meaning | +|---------|---------| +| `resource:read` | Read-only access (current user) | +| `resource:write` | Read and write access (current user) | +| `resource:master` | Full access including delete (current user) | +| `resource:read:admin` | Read-only access (all account users) | +| `resource:write:admin` | Read and write access (all account users) | +| `resource:master:admin` | Full access including delete (all account users) | + +## Best Practices + +1. **Request minimum scopes** - Only what you need +2. **Explain to users** - Why you need each scope +3. **Handle denied scopes** - Graceful fallback + +## Resources + +- **Scopes reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ diff --git a/partner-built/zoom-plugin/skills/general/references/sdk-logs-troubleshooting.md b/partner-built/zoom-plugin/skills/general/references/sdk-logs-troubleshooting.md new file mode 100644 index 00000000..a21b249e --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/sdk-logs-troubleshooting.md @@ -0,0 +1,194 @@ +# SDK Logs & Troubleshooting + +Collecting SDK logs for debugging and support. + +## Official Log Retrieval Guides + +**IMPORTANT**: Always refer to the official Zoom log retrieval guides for the most up-to-date instructions: + +- **Video SDK Log Retrieval**: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/ +- **Meeting SDK Log Retrieval**: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/ + +If these URLs are unavailable, search for "zoom sdk log retrieval" to find the current documentation. + +## Overview + +SDK logs help diagnose issues during development and for Zoom support escalations. + +## Enabling Logs + +### Web SDK + +```javascript +// Enable verbose logging +ZoomMtg.setLogLevel('verbose'); + +// Or for Video SDK +client.init('en-US', 'CDN', { debug: true }); +``` + +**Web Tracking ID**: For Web SDK troubleshooting, get the **Web Tracking ID** which helps Zoom support trace your session. + +**Meeting SDK Web**: +1. Open browser DevTools → **Network** tab +2. Look for a request starting with `info?meetingNumber...` +3. Click on the request and check the **Response Headers** +4. Find the `x-zm-trackingid` header value +5. Copy this ID for support tickets + +**Video SDK Web**: +1. Open browser DevTools → **Network** tab +2. Look for a request starting with `lsdk?topic...` +3. Click on the request and check the **Response Headers** +4. Find the `x-zm-trackingid` header value +5. Copy this ID for support tickets + +``` +Example header: +x-zm-trackingid: v=2.0;clid=us04;rid=WEB_abc123xyz... +``` + +The Web Tracking ID is essential for Zoom support to investigate Web SDK issues. + +**To get help with logs and tracking IDs:** +- **Open a support ticket**: https://devsupport.zoom.us/ +- **Post on Developer Forum**: https://devforum.zoom.us/ + +Include the tracking ID and relevant logs when requesting assistance. + +### iOS SDK + +```swift +// Set log file path +let initParams = MobileRTCSDKInitParams() +initParams.enableLog = true +initParams.logFilePrefix = "zoom_sdk" +``` + +### Android SDK + +```kotlin +val initParams = ZoomSDKInitParams().apply { + enableLog = true + logSize = 5 // MB +} +``` + +### Desktop SDKs (Windows/macOS/Linux) + +```cpp +initParam.enableLogByDefault = true; +initParam.logFilePrefix = L"zoom_sdk"; +``` + +## Log Locations + +| Platform | Default Location | +|----------|------------------| +| iOS | App's Documents directory | +| Android | App's files directory | +| Windows | `%APPDATA%\ZoomSDK\` | +| macOS | `~/Library/Logs/ZoomSDK/` | +| Linux | Working directory | + +## Common Issues and Solutions + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Join failed | Invalid signature | Check JWT generation (exp should be ~10s after iat, but iat can be up to 2 hours in past) | +| Join failed | Meeting not found | Verify meeting number and that meeting hasn't ended | +| No audio | Permission denied | Request microphone permission before joining | +| No video | Permission denied | Request camera permission before joining | +| Video scales down | Container too small | Ensure container is at least 1280x720 for 720p | +| SharedArrayBuffer error | Missing headers | Add COOP/COEP headers to server | +| Error code 0 | Actually success | Check SDK docs - 0 often means success, not error | +| SDK crash | ProGuard enabled | Disable ProGuard/R8 for Zoom SDK classes | +| DLL not found | Missing files | Copy ALL DLLs from SDK bin folder | + +### Debugging Join Failures + +```javascript +// Web SDK - enable verbose logging +ZoomMtg.setLogLevel('verbose'); + +// Check signature +console.log('Signature:', signature); +console.log('Meeting:', meetingNumber); + +// Verify callback +client.join({ + // ...params + success: (res) => console.log('Join success:', res), + error: (err) => console.error('Join error:', err) +}); +``` + +### Debugging Audio/Video Issues + +```javascript +// Check device availability +const devices = await navigator.mediaDevices.enumerateDevices(); +console.log('Audio inputs:', devices.filter(d => d.kind === 'audioinput')); +console.log('Video inputs:', devices.filter(d => d.kind === 'videoinput')); + +// Check permissions +const micPermission = await navigator.permissions.query({ name: 'microphone' }); +const camPermission = await navigator.permissions.query({ name: 'camera' }); +console.log('Mic:', micPermission.state); +console.log('Cam:', camPermission.state); +``` + +### Native SDK Crash Debugging + +**iOS**: +```swift +// Enable crash reporting +MobileRTC.shared().setEnableCrashReport(true) + +// Get logs +let logPath = MobileRTC.shared().getLogPath() +print("Logs at: \(logPath)") +``` + +**Android**: +```kotlin +// Check logcat for crashes +adb logcat -s ZoomSDK:V + +// Disable ProGuard for Zoom +// In proguard-rules.pro: +-keep class us.zoom.** { *; } +-keep interface us.zoom.** { *; } +``` + +### Error Code Reference + +| Code | Meaning | Platform | +|------|---------|----------| +| 0 | Success (NOT error!) | All | +| 1 | Generic error | All | +| 2 | Invalid argument | All | +| 3 | Invalid token | All | +| 4 | Timeout | All | +| 8 | SDK not authorized | Windows | +| 100000400 | Meeting join failed | Windows | +| MobileRTCMeetError_Success | Success | iOS | +| MEETING_STATUS_INMEETING | In meeting | Android | + +## Getting Support + +When contacting Zoom support: + +1. Include SDK version +2. Attach log files +3. Describe steps to reproduce +4. Include error codes (check if 0 means success!) + +## Resources + +- **Video SDK Log Retrieval Guide**: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/ +- **Meeting SDK Log Retrieval Guide**: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/ +- **Developer support**: https://devsupport.zoom.us/ +- **Developer forum**: https://devforum.zoom.us/ + +> **Note for LLMs/Agents**: If the log retrieval URLs return 404 or have moved, perform a web search for "zoom sdk log retrieval instructions" to find the current documentation. diff --git a/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-guide.md b/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-guide.md new file mode 100644 index 00000000..6e97fdb5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-guide.md @@ -0,0 +1,164 @@ +# SDK Upgrade Guide + +Guide for upgrading Meeting SDK and Video SDK versions. + +For customer upgrades from older versions to latest, use: +- [sdk-upgrade-workflow.md](sdk-upgrade-workflow.md) - changelog + RSS, version-by-version migration workflow. + +## IMPORTANT: Check the Changelog First + +**Before any upgrade, always check the official Zoom changelog:** + +**Primary URL**: https://developers.zoom.us/changelog/ + +If the above URL is unavailable or has moved, search for **"zoom changelog"** or **"zoom developer changelog"** to find the current location. + +The changelog contains: +- Latest SDK versions and release dates +- Breaking changes and deprecations +- New features and improvements +- Bug fixes and security patches + +## Overview + +Zoom releases SDK updates regularly. This guide covers version policy and upgrade procedures. + +## Version Policy + +- **Major versions** - May contain breaking changes +- **Minor versions** - New features, backward compatible +- **Patch versions** - Bug fixes + +## Before Upgrading + +1. Read changelog for target version +2. Note breaking changes and deprecations +3. Test in development environment +4. Plan migration for deprecated APIs + +## Upgrade Steps + +### Web SDK (npm) + +```bash +# Check current version +npm list @zoom/meetingsdk + +# Update to latest +npm update @zoom/meetingsdk + +# Or specific version +npm install @zoom/meetingsdk@2.18.0 +``` + +### Native SDKs + +1. Download new SDK from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +2. Replace SDK files in your project +3. Update linker/framework settings if needed +4. Rebuild project + +## Common Migration Tasks + +### API Signature Changes + +When methods change signatures between versions: + +```javascript +// Old (v2.x) +client.join({ + sdkKey: key, + sdkSecret: secret, // REMOVED in v3.x + meetingNumber: number +}); + +// New (v3.x) - signature generated server-side +client.join({ + sdkKey: key, + signature: serverGeneratedSignature, // NEW + meetingNumber: number +}); +``` + +**Action**: Update to server-side signature generation for security. + +### Deprecated Method Replacements + +| Old Method | New Method | Version | +|------------|------------|---------| +| `ZoomMtg.init()` | `client.init()` | Web SDK 3.x | +| `startVideo()` | `startVideo()` + `renderVideo()` | Video SDK 1.8+ | +| `getMeetingUUID()` | Use webhook payload | Meeting SDK 2.x | + +### New Initialization Requirements + +**Meeting SDK Web 3.x**: +```javascript +// Now requires explicit preload +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +// Must init before join +await client.init({ + zoomAppRoot: document.getElementById('root'), + language: 'en-US' +}); +``` + +**Video SDK 1.8+**: +```javascript +// Video rendering is now two-step +await stream.startVideo(); +await stream.renderVideo( + document.querySelector('#video-canvas'), + myUserId, + 1280, 720, 0, 0, 3 // width, height, x, y, quality +); +``` + +### Breaking Changes Checklist + +When upgrading major versions, check: + +- [ ] Initialization flow changed? +- [ ] Authentication method changed? +- [ ] Event names/signatures changed? +- [ ] Required permissions changed? +- [ ] Minimum platform version changed? +- [ ] New required headers (COOP/COEP)? + +### Testing Upgrade + +```bash +# Create upgrade branch +git checkout -b sdk-upgrade-v3 + +# Update package +npm install @zoom/meetingsdk@latest + +# Run tests +npm test + +# Test manually +# - Join meeting +# - Audio/video functionality +# - Screen sharing +# - Recording (if used) +# - Custom UI features +``` + +## Version Support Policy + +- **Latest version**: Full support +- **Previous major**: Security fixes only +- **Older versions**: No support, upgrade recommended + +## Resources + +- **Main Changelog**: https://developers.zoom.us/changelog/ (check here first!) +- **Meeting SDK changelog**: https://developers.zoom.us/changelog/meeting-sdk/ +- **Video SDK changelog**: https://developers.zoom.us/changelog/video-sdk/ +- **Migration guides**: https://developers.zoom.us/docs/meeting-sdk/web/migrate/ + +> **Note for LLMs/Agents**: If the changelog URLs return 404 or have moved, perform a web search for "zoom developer changelog" or "zoom sdk changelog" to find the current location. Zoom occasionally restructures their documentation. diff --git a/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-workflow.md b/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-workflow.md new file mode 100644 index 00000000..f4e05e57 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/references/sdk-upgrade-workflow.md @@ -0,0 +1,144 @@ +# SDK Upgrade Workflow (Changelog + RSS) + +Reusable process for upgrading Zoom SDK integrations from an older customer version to latest with low regression risk. + +## Use This When + +- Customer is multiple versions behind. +- Breaking changes may exist between current and latest. +- You need a defensible, version-by-version upgrade plan. + +## Inputs Required + +1. Product and platform +- Example: `Meeting SDK Android`, `Video SDK iOS`, `Contact Center Web`. + +2. Current version in production +- Example: `6.3.1`, `2.1.0`. + +3. Target version +- Usually latest stable from changelog. + +4. Critical features in use +- Example: custom UI, raw data, recording, chat, live transcription, token flow. + +## Canonical Source + +- Changelog entry point: https://developers.zoom.us/changelog/ + +## Workflow + +### 1) Scope the upgrade lane + +- Confirm exact product + platform lane before collecting releases. +- Do not mix lanes (for example, Meeting SDK Web and Meeting SDK iOS must be treated separately). + +### 2) Locate the platform-specific RSS feed + +From `https://developers.zoom.us/changelog/`: +- Filter by product/platform. +- Find the RSS link for that filtered lane. +- Use only that feed for release collection. + +If feed discovery is unclear: +- Open the filtered changelog page and locate the RSS icon/link. +- Confirm feed entries match the same product/platform lane. + +### 3) Build the release ledger + +Collect all releases from: +- `current_version` (exclusive) up to `target_version` (inclusive), then latest if target is `latest`. + +For each release entry capture: +- Version +- Release date +- Release URL +- Breaking/deprecated notes +- Required migration actions + +Sort upgrade steps in ascending version order. + +### 4) Plan upgrade hops + +Default strategy: +- Patch/minor jumps can often be grouped. +- Major changes should be isolated into dedicated hops. + +Recommended hop pattern: +1. `current -> next safe checkpoint` +2. `checkpoint -> next major boundary` +3. Repeat until latest + +### 5) Extract required actions per hop + +For each hop, classify actions under: +- Auth/token contract changes +- API renames/signature changes +- Initialization/lifecycle changes +- Event payload/callback changes +- Build/dependency/runtime requirements +- Feature removals/deprecations + +### 6) Apply compatibility guards + +- Wrap renamed/deprecated calls behind adapters. +- Keep temporary compatibility mappings for payload changes. +- Add feature flags for behavior toggles when needed. + +### 7) Validate each hop before continuing + +Minimum validation set: +- SDK init/auth +- Join/start/session entry flow +- Core media flows (audio/video/share) if applicable +- Critical product-specific features used by customer +- Cleanup/leave/disconnect behavior + +Do not skip to next hop if the current hop is unstable. + +### 8) Produce final upgrade package + +Deliver: +- Step-by-step upgrade matrix +- Per-hop code/config change list +- Deprecated-to-replacement map +- Risks and rollback notes +- Final target-state checklist + +## Output Template + +```markdown +## Upgrade Plan: + +- Current: +- Target: +- Source feed: + +### Hop 1: x.y+1.z> +- Release notes: + - +- Breaking/deprecations: + - +- Required changes: + - +- Validation: + - + +### Hop 2: <...> +... + +## Deprecated -> Replacement Map +- -> + +## Risks +- + +## Rollback +- +``` + +## Operating Rules + +- Never assume only latest release notes are sufficient. +- Always process intermediate releases between customer version and target. +- Prefer smallest-risk path over fastest path for production upgrades. diff --git a/partner-built/zoom-plugin/skills/general/use-cases/ai-companion-integration.md b/partner-built/zoom-plugin/skills/general/use-cases/ai-companion-integration.md new file mode 100644 index 00000000..22e40967 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/ai-companion-integration.md @@ -0,0 +1,392 @@ +# AI Companion Integration + +Integrate with Zoom AI Companion for meeting summaries, transcripts, and AI-powered features. + +## Overview + +Zoom AI Companion provides AI-powered features including: +- Meeting summaries (auto-generated) +- Meeting transcripts +- Real-time transcription +- Smart recording highlights +- Conversation archives + +## What's Available via API + +| Feature | API Access | Method | +|---------|------------|--------| +| Meeting Summaries | ✅ Yes | REST API | +| Meeting Transcripts | ✅ Yes | REST API (Cloud Recording) | +| Real-Time Transcripts | ✅ Yes | RTMS SDK | +| AI Companion Panel | ⚠️ Limited | Archive only | +| Conversation Archives | ✅ Yes | REST API | +| AI Controls in Meeting | ✅ Yes | Meeting SDK | + +## Skills Needed + +| Use Case | Skills | +|----------|--------| +| AI-agent search and tool invocation over Zoom meeting context | **zoom-mcp** | +| Get meeting summaries after meeting | **zoom-rest-api** | +| Get meeting transcripts in deterministic backend pipeline | **zoom-rest-api** + **zoom-webhooks** | +| Real-time transcript streaming | **rtms** | +| Control AI features in embedded meetings | **zoom-meeting-sdk** | + +## Routing Modes + +- Use **zoom-rest-api** when you need deterministic backend jobs, strict retry behavior, and explicit endpoint control. +- Use **zoom-mcp** when an AI system must discover and invoke Zoom tools dynamically. +- Use both when your architecture requires stable API automation plus AI-driven retrieval and assistance. + +--- + +## Get Meeting Summary (REST API) + +### Endpoint + +``` +GET /v2/meetings/{meetingUUID}/meeting_summary +``` + +### Prerequisites + +1. Meeting Summary feature enabled in account settings +2. Server-to-Server OAuth app with admin scopes +3. Meeting must have ended with summary generated + +### Example + +```javascript +// Get meeting summary +async function getMeetingSummary(meetingUUID) { + // Double-encode UUID if it contains / or // + const encodedUUID = meetingUUID.startsWith('/') + ? encodeURIComponent(encodeURIComponent(meetingUUID)) + : encodeURIComponent(meetingUUID); + + const response = await fetch( + `https://api.zoom.us/v2/meetings/${encodedUUID}/meeting_summary`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.json(); +} + +// Response example +{ + "meeting_uuid": "abc123...", + "meeting_id": 12345678901, + "meeting_topic": "Weekly Team Sync", + "meeting_start_time": "2024-01-15T10:00:00Z", + "meeting_end_time": "2024-01-15T10:45:00Z", + "summary_start_time": "2024-01-15T10:00:00Z", + "summary_end_time": "2024-01-15T10:45:00Z", + "summary_content": { + "summary": "The team discussed Q1 roadmap priorities...", + "next_steps": [ + "John to finalize design specs by Friday", + "Sarah to schedule customer interviews" + ], + "keywords": ["roadmap", "Q1", "design", "customers"] + } +} +``` + +### Important Notes + +- **Admin role required**: Standard users may not have access +- **Make app admin-managed**: Resolves permission issues +- **UUID encoding**: Double-encode UUIDs starting with `/` + +--- + +## Get Meeting Transcript (REST API) + +Transcripts are accessed via the Cloud Recording API. + +### Endpoint + +``` +GET /v2/meetings/{meetingId}/recordings +``` + +### Example + +```javascript +async function getMeetingTranscript(meetingId) { + const response = await fetch( + `https://api.zoom.us/v2/meetings/${meetingId}/recordings`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const data = await response.json(); + + // Find transcript files + const transcriptFiles = data.recording_files.filter( + file => file.file_type === 'TRANSCRIPT' || + file.file_type === 'CC' || + file.file_type === 'SUMMARY' + ); + + // Download transcript + for (const file of transcriptFiles) { + const transcriptResponse = await fetch(file.download_url, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + if (file.file_extension === 'VTT') { + const vttContent = await transcriptResponse.text(); + console.log('VTT Transcript:', vttContent); + } else if (file.file_extension === 'JSON') { + const jsonContent = await transcriptResponse.json(); + console.log('JSON Transcript:', jsonContent); + } + } + + return transcriptFiles; +} +``` + +### Transcript File Types + +| File Type | Extension | Description | +|-----------|-----------|-------------| +| `TRANSCRIPT` | VTT, JSON | Full meeting transcript | +| `CC` | VTT | Closed captions | +| `SUMMARY` | JSON | AI-generated summary | + +--- + +## Webhooks for AI Content + +Listen for when AI content is ready: + +```javascript +// Webhook handler +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'recording.transcript_completed': + // Transcript is ready + console.log('Transcript ready for meeting:', payload.object.uuid); + fetchAndStoreTranscript(payload.object.uuid); + break; + + case 'recording.completed': + // Recording processing complete (may include summary) + console.log('Recording ready:', payload.object.uuid); + break; + + case 'meeting.ended': + // Meeting ended - summary will be generated soon + console.log('Meeting ended:', payload.object.uuid); + break; + } + + res.sendStatus(200); +}); +``` + +--- + +## Real-Time Transcripts (RTMS) + +For live transcript streaming during meetings, use RTMS SDK. + +### Prerequisites + +- RTMS access approval from Zoom +- Server infrastructure for WebSocket connections + +### Example + +```javascript +import { RTMSClient } from "@zoom/rtms"; + +const client = new RTMSClient({ + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + secretToken: process.env.ZOOM_SECRET_TOKEN +}); + +// Connect to meeting +await client.joinMeeting({ + meetingUuid: meetingUUID, + streamId: streamId, + serverUrl: "wss://rtms.zoom.us" +}); + +// Listen for transcript events +client.on('transcript', (data) => { + console.log(`[${data.speakerName}]: ${data.text}`); + + // Process real-time transcript + // - Send to AI for sentiment analysis + // - Display live captions + // - Log for compliance +}); +``` + +See **rtms** skill for full RTMS documentation. + +--- + +## Meeting SDK - AI Companion Controls + +Control AI Companion features in embedded meetings. + +### Web SDK + +```javascript +// Check if AI Companion is available +const aiCompanionStatus = ZoomMtg.getAICompanionStatus(); + +// AI Companion features are controlled by meeting settings +// The SDK respects account/meeting-level AI Companion settings +``` + +### Native SDKs (Android/iOS/Desktop) + +Use `InMeetingAICompanionController`: + +```java +// Android example +InMeetingAICompanionController aiController = + ZoomSDK.getInstance().getInMeetingService().getInMeetingAICompanionController(); + +// Check AI Companion status +boolean isEnabled = aiController.isAICompanionEnabled(); + +// Get available features +AICompanionFeature[] features = aiController.getAvailableFeatures(); +// Features: QUERY, SMART_SUMMARY, SMART_RECORDING +``` + +```swift +// iOS example +let aiController = MobileRTC.shared().getMeetingService()?.getInMeetingAICompanionController() + +if let isEnabled = aiController?.isAICompanionEnabled() { + print("AI Companion enabled: \(isEnabled)") +} +``` + +### Feature Constants + +| Feature | Description | +|---------|-------------| +| `QUERY` | Ask AI Companion questions | +| `SMART_SUMMARY` | Meeting summary generation | +| `SMART_RECORDING` | Smart recording highlights | + +--- + +## Conversation Archives API + +Archive AI Companion panel conversations (new September 2025). + +```javascript +// Get conversation archives +async function getConversationArchives(userId) { + const response = await fetch( + `https://api.zoom.us/v2/users/${userId}/conversation_archive`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.json(); +} +``` + +--- + +## Common Integration Patterns + +### Pattern 1: Post-Meeting Summary Pipeline + +```javascript +// 1. Listen for meeting end +webhooks.on('meeting.ended', async (meeting) => { + // 2. Wait for transcript to be ready (or use webhook) + await delay(60000); // Processing time varies + + // 3. Fetch summary + const summary = await getMeetingSummary(meeting.uuid); + + // 4. Store or distribute + await saveSummaryToDatabase(summary); + await sendSummaryToParticipants(meeting.participants, summary); +}); +``` + +### Pattern 2: Real-Time AI Processing + +```javascript +// Using RTMS for live processing +rtmsClient.on('transcript', async (data) => { + // Send to your AI service for analysis + const sentiment = await analyzesentiment(data.text); + const actionItems = await extractActionItems(data.text); + + // Update live dashboard + updateDashboard({ sentiment, actionItems }); +}); +``` + +### Pattern 3: Compliance Archival + +```javascript +// Archive all AI-generated content +async function archiveMeetingAIContent(meetingId) { + const [summary, transcript, archives] = await Promise.all([ + getMeetingSummary(meetingId), + getMeetingTranscript(meetingId), + getConversationArchives(meetingId) + ]); + + await complianceStore.save({ + meetingId, + summary, + transcript, + aiConversations: archives, + archivedAt: new Date() + }); +} +``` + +--- + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `meeting:read:admin` | Read meeting data including summaries | +| `recording:read:admin` | Access recordings and transcripts | +| `user:read:admin` | Read user data for archives | + +--- + +## Limitations + +| Limitation | Notes | +|------------|-------| +| AI Companion Panel | Most panel features NOT available via API | +| Admin access | Some endpoints require admin role | +| Processing time | Summaries/transcripts not instant after meeting | +| RTMS approval | Real-time access requires Zoom approval | +| Bot restrictions | Meeting SDK does NOT support bots (use RTMS) | + +--- + +## Resources + +- **AI Companion APIs**: https://developers.zoom.us/docs/api/ai-companion/ +- **Meeting Summary API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingSummary +- **Cloud Recording API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **RTMS Documentation**: https://developers.zoom.us/docs/rtms/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/ai-integration.md b/partner-built/zoom-plugin/skills/general/use-cases/ai-integration.md new file mode 100644 index 00000000..48d07d73 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/ai-integration.md @@ -0,0 +1,224 @@ +# AI Integration + +Build real-time AI features for Zoom meetings - sentiment analysis, summarization, and more. + +## Overview + +Integrate AI/ML capabilities with Zoom meetings using real-time media streams for live transcription, sentiment analysis, meeting summarization, and intelligent automation. + +## Skills Needed + +- **rtms** - Primary (real-time media access) +- **zoom-meeting-sdk** (Linux) - For meeting bots + +## AI Use Cases + +| Use Case | Input | Output | +|----------|-------|--------| +| Transcription | Audio stream | Real-time text | +| Sentiment | Audio/transcript | Mood indicators | +| Summarization | Transcript | Meeting summary | +| Action items | Transcript | Task list | +| Translation | Audio/transcript | Multi-language | + +## Architecture + +``` +AI Integration Architecture: +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Zoom │────▶│ RTMS / │────▶│ AI/ML │ +│ Meeting │ │ Bot SDK │ │ Pipeline │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + Audio/Video/Transcript +``` + +## Prerequisites + +- RTMS access or Meeting SDK (Linux) +- AI/ML service (OpenAI, Azure, custom) +- Real-time processing infrastructure + +## Common Tasks + +### Setting Up RTMS for AI + +```javascript +// 1. Configure RTMS app in Marketplace +// Enable: Audio stream, Video stream, Transcript stream + +// 2. Handle webhook to get connection details +app.post('/webhook', (req, res) => { + if (req.body.event === 'meeting.rtms_started') { + const { server_urls, stream_id, signature } = req.body.payload; + + // Start AI processing pipeline + aiPipeline.connect({ + url: server_urls[0], + streamId: stream_id, + signature: signature + }); + } + res.status(200).send(); +}); +``` + +### Real-Time Transcription Pipeline + +```javascript +// Option 1: Use Zoom's built-in transcript (via RTMS) +rtmsClient.on('transcript', (data) => { + const { text, speaker_id, is_final } = data; + if (is_final) { + transcriptStore.append(speaker_id, text); + } +}); + +// Option 2: Send audio to external STT (Whisper, Deepgram) +const deepgram = new Deepgram(DEEPGRAM_KEY); +const transcriber = deepgram.transcription.live({ + punctuate: true, + interim_results: true, + language: 'en-US' +}); + +rtmsClient.on('audio', (audioChunk) => { + transcriber.send(audioChunk); +}); + +transcriber.on('transcriptReceived', (data) => { + const transcript = data.channel.alternatives[0].transcript; + processTranscript(transcript); +}); +``` + +### Sentiment Analysis Integration + +```javascript +// Real-time sentiment on transcript segments +async function analyzeSentiment(text) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Analyze sentiment. Return JSON: {sentiment: "positive|negative|neutral", confidence: 0-1, emotions: []}' + }, { + role: 'user', + content: text + }], + response_format: { type: 'json_object' } + }); + + return JSON.parse(response.choices[0].message.content); +} + +// Track sentiment over time +class SentimentTracker { + constructor() { + this.history = []; + } + + async process(transcript) { + const sentiment = await analyzeSentiment(transcript); + this.history.push({ + timestamp: Date.now(), + text: transcript, + ...sentiment + }); + + // Alert on negative sentiment + if (sentiment.sentiment === 'negative' && sentiment.confidence > 0.8) { + this.emit('alert', { type: 'negative_sentiment', data: sentiment }); + } + } + + getOverallSentiment() { + // Aggregate sentiment over meeting duration + } +} +``` + +### Meeting Summarization + +```javascript +// Generate summary after meeting ends +async function generateMeetingSummary(fullTranscript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: `Summarize this meeting transcript. Include: + 1. Key discussion points + 2. Decisions made + 3. Action items with owners + 4. Follow-up needed` + }, { + role: 'user', + content: fullTranscript + }] + }); + + return response.choices[0].message.content; +} + +// Extract action items +async function extractActionItems(transcript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Extract action items as JSON array: [{task, owner, deadline}]' + }, { + role: 'user', + content: transcript + }], + response_format: { type: 'json_object' } + }); + + return JSON.parse(response.choices[0].message.content); +} +``` + +### Latency Considerations + +| Processing Type | Target Latency | Recommendation | +|-----------------|----------------|----------------| +| Live captions | < 500ms | Use streaming STT (Deepgram, AssemblyAI) | +| Sentiment | < 2s | Batch every 10-15 seconds | +| Summarization | Post-meeting | Process after meeting ends | +| Action items | < 5s | Process paragraph by paragraph | + +### Example AI Pipeline Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ RTMS WebSocket │ +└─────────────┬───────────────┬───────────────┬──────────┘ + │ │ │ + Audio Stream Video Stream Transcript + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌──────────┐ ┌───────────────┐ + │ Speech-to-Text│ │Face/OCR │ │ NLP Pipeline │ + │ (Deepgram) │ │Detection │ │ (OpenAI GPT) │ + └───────┬───────┘ └────┬─────┘ └───────┬───────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ Results Aggregator │ + │ - Transcripts - Sentiment - Action Items │ + └─────────────────────┬───────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Storage / │ + │ Dashboard │ + └───────────────┘ +``` + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **Meeting SDK Linux**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Deepgram**: https://deepgram.com/ +- **OpenAI API**: https://platform.openai.com/docs diff --git a/partner-built/zoom-plugin/skills/general/use-cases/apis-vs-mcp-routing.md b/partner-built/zoom-plugin/skills/general/use-cases/apis-vs-mcp-routing.md new file mode 100644 index 00000000..4c0c76e7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/apis-vs-mcp-routing.md @@ -0,0 +1,83 @@ +# APIs vs MCP Routing + +Decide whether to route a request to Zoom APIs, Zoom MCP, or both. + +## Overview + +Zoom APIs and Zoom MCP are complementary: + +- Zoom APIs are best for deterministic system integrations. +- Zoom MCP is best for AI-driven tool-based workflows. +- Use both for enterprise AI systems that need a stable automation core and an adaptive AI layer. +- Zoom-hosted MCP follows a product-scoped server model; access is OAuth-scoped and governed. + +## Decision Matrix + +| Primary requirement | Route | Notes | +|---------------------|-------|-------| +| Deterministic automation, configuration, reporting, scheduled jobs, strict retries/error handling | **zoom-rest-api** | Direct control over requests, retries, and idempotency | +| AI interaction, dynamic tool discovery, AI Companion workflows, external AI interoperability | **zoom-mcp** | Agent chooses tools contextually through MCP | +| High-volume production automation plus AI assistant workflows | **zoom-rest-api + zoom-mcp** | Keep core actions in APIs; expose curated tool surfaces via MCP | + +## Typical Routing Examples + +| User request | Route | +|--------------|-------| +| "Create meetings nightly and sync metrics to BI" | **zoom-rest-api** | +| "Let my assistant search meeting content and fetch transcripts" | **zoom-mcp** | +| "Automate meeting lifecycle, then let agents answer questions from summaries" | **zoom-rest-api + zoom-mcp** | + +## Chaining Patterns + +### Pattern A: API-only deterministic backend + +1. `zoom-oauth` for app auth/token lifecycle. +2. `zoom-rest-api` for create/read/update/reporting endpoints. +3. `zoom-webhooks` for async event processing if needed. + +### Pattern B: MCP-first AI tool workflows + +1. `zoom-oauth` for user OAuth token required by MCP server. +2. `zoom-mcp` for semantic meeting search, summaries, recordings/transcripts, and tool invocation. + +### Pattern C: Hybrid enterprise AI architecture + +1. `zoom-rest-api` handles provisioning, policy/configuration, and scheduled ingestion jobs. +2. `zoom-webhooks` or `zoom-websockets` handles event ingestion. +3. `zoom-mcp` exposes curated higher-level tools for AI Companion or external agents. + +## MCP Fit Checklist (FAQ-Aligned) + +Use `zoom-mcp` when you are: + +- Building custom tools for AI models. +- Creating data integration services for AI assistants. +- Developing specialized assistants that need tool discovery. +- Extending AI capabilities with external services via MCP. +- Building enterprise AI solutions that need interoperable agent tooling. + +## MCP Client and Transport Constraints + +- Zoom remote MCP server is consumed over Streamable HTTP/SSE. +- Typical supported MCP clients include Claude and VS Code MCP-capable tooling. +- A local stdio mode may be available depending on client setup, but remote Zoom MCP routing assumes HTTP/SSE transport. +- Endpoint model is shared by instance/cluster; do not assume per-customer dedicated endpoint generation. +- MCP server surfaces can be product-scoped (for example Meetings, Team Chat, Whiteboard). Route by product when those surfaces are available. + +## Routing Guardrails + +- Do not route deterministic backend automation to MCP only. +- Do not route AI-agent tool discovery tasks to REST only. +- Prefer hybrid routing when both deterministic backend operations and AI-driven interactions are required. + +## Related Skills + +- [zoom-rest-api](../../rest-api/SKILL.md) +- [zoom-mcp](../../zoom-mcp/SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) +- [zoom-webhooks](../../webhooks/SKILL.md) +- [zoom-websockets](../../websockets/SKILL.md) + +## Source + +- https://developers.zoom.us/docs/mcp/library/resources/apis-vs-mcp/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/backend-automation-s2s-oauth.md b/partner-built/zoom-plugin/skills/general/use-cases/backend-automation-s2s-oauth.md new file mode 100644 index 00000000..ac174c46 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/backend-automation-s2s-oauth.md @@ -0,0 +1,221 @@ +# Backend Automation with Server-to-Server OAuth + +Automate Zoom account operations using Server-to-Server OAuth for machine-to-machine authentication. + +## Scenario + +You're building a backend service that needs to: +- Automatically create and manage meetings for your organization +- Generate meeting reports +- Provision and deprovision users +- No user interaction required +- Account-wide API access + +## Required Skills + +1. **oauth** - S2S OAuth token management +2. **zoom-rest-api** - Account management endpoints + +## Architecture + +``` +Cron Job / Backend Service + ↓ + Token Cache (Redis) + ↓ + Zoom APIs (account-wide access) +``` + +## Implementation + +### 1. S2S OAuth Setup (oauth) + +**Configure app in Zoom Marketplace:** +- App Type: Server-to-Server OAuth +- Add required scopes: `meeting:write:admin`, `user:write:admin`, `report:read:admin` +- Get credentials: Account ID, Client ID, Client Secret + +**See:** `oauth/concepts/oauth-flows.md#server-to-server-s2s-oauth` + +### 2. Token Management with Redis (oauth) + +```javascript +const redis = require('redis'); +const client = redis.createClient(); + +async function getZoomToken() { + // Check cache first + let token = await client.get('zoom_s2s_token'); + + if (!token) { + // Request new token + const response = await axios.post( + 'https://zoom.us/oauth/token', + 'grant_type=account_credentials&account_id=' + ACCOUNT_ID, + { + headers: { + 'Authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64') + } + } + ); + + token = response.data.access_token; + + // Cache with TTL (10 second buffer before actual expiration) + await client.setex('zoom_s2s_token', response.data.expires_in - 10, token); + } + + return token; +} +``` + +**See:** `oauth/examples/s2s-oauth-redis.md` + +### 3. Automated User Provisioning (zoom-rest-api) + +```javascript +// Daily cron job to sync users +cron.schedule('0 0 * * *', async () => { + const token = await getZoomToken(); + + const newUsers = await getNewUsersFromHR(); + + for (const user of newUsers) { + await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', + user_info: { + email: user.email, + type: 1, + first_name: user.firstName, + last_name: user.lastName + } + }, + { + headers: { Authorization: `Bearer ${token}` } + } + ); + } +}); +``` + +**Chain to:** `zoom-rest-api` for endpoint details + +### 4. Meeting Reports (zoom-rest-api) + +```javascript +// Generate weekly meeting reports +async function generateWeeklyReport() { + const token = await getZoomToken(); + + const response = await axios.get( + 'https://api.zoom.us/v2/report/users', + { + params: { + from: startOfWeek(), + to: endOfWeek() + }, + headers: { Authorization: `Bearer ${token}` } + } + ); + + return response.data.users; +} +``` + +**Chain to:** `zoom-rest-api` reporting endpoints + +## Production Deployment + +### Docker Setup (oauth) + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:7-alpine + + automation-service: + build: . + environment: + - ZOOM_ACCOUNT_ID=${ZOOM_ACCOUNT_ID} + - ZOOM_CLIENT_ID=${ZOOM_CLIENT_ID} + - ZOOM_CLIENT_SECRET=${ZOOM_CLIENT_SECRET} + - REDIS_URL=redis://redis:6379 + depends_on: + - redis +``` + +**See:** `oauth/examples/s2s-oauth-redis.md#docker-deployment` + +## Error Handling + +### Token Errors (oauth) + +```javascript +try { + const token = await getZoomToken(); +} catch (error) { + if (error.response?.data?.error === 'invalid_client') { + // Invalid credentials + logger.error('Invalid Zoom credentials'); + alertOps('Zoom integration broken - check credentials'); + } +} +``` + +**See:** `oauth/troubleshooting/common-errors.md` + +### Rate Limiting (zoom-rest-api) + +```javascript +// Implement retry logic for rate limits +const retryRequest = async (fn, retries = 3) => { + for (let i = 0; i < retries; i++) { + try { + return await fn(); + } catch (error) { + if (error.response?.status === 429) { + // Rate limited - wait and retry + await sleep(Math.pow(2, i) * 1000); + continue; + } + throw error; + } + } +}; +``` + +**Chain to:** `zoom-rest-api` for rate limit details + +## Testing + +```javascript +// Test S2S token acquisition +describe('S2S OAuth', () => { + it('should get valid access token', async () => { + const token = await getZoomToken(); + expect(token).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + + it('should cache token in Redis', async () => { + await getZoomToken(); + const cached = await client.get('zoom_s2s_token'); + expect(cached).toBeTruthy(); + }); +}); +``` + +## Related Use Cases + +- `meeting-automation.md` - Advanced meeting workflows +- `usage-reporting-analytics.md` - Account usage analytics +- `user-and-meeting-creation.md` - Bulk operations + +## Skills Used + +- **oauth** (primary) - S2S OAuth, token caching +- **zoom-rest-api** - Account management, reporting +- **webhooks** - Real-time event notifications diff --git a/partner-built/zoom-plugin/skills/general/use-cases/collaborative-apps.md b/partner-built/zoom-plugin/skills/general/use-cases/collaborative-apps.md new file mode 100644 index 00000000..a4d374d2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/collaborative-apps.md @@ -0,0 +1,89 @@ +# Collaborative Apps + +Build real-time shared experiences across meeting participants. + +## Overview + +Collaborative Zoom Apps let multiple participants interact with the same shared state simultaneously - like Google Docs for Zoom meetings. Examples: shared whiteboards, polls, text editors, dashboards. + +## Skills Needed + +- **zoom-apps-sdk** (Collaborate Mode, App Communication) - Primary +- **oauth** - Authentication + +## Synchronization Patterns + +| Pattern | Technology | Best For | Complexity | +|---------|-----------|----------|------------| +| **SDK messaging** | connect() + postMessage() | Simple state (counters, toggles) | Low | +| **Server relay** | Socket.io / WebSocket | Polls, games, dashboards | Medium | +| **CRDT sync** | Y.js + WebRTC | Text editors, whiteboards | High | + +### Pattern 1: SDK Messaging (Simplest) + +No server needed for state sync: + +```javascript +await zoomSdk.connect(); + +// Send state change +await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'vote', option: 'A' }) +}); + +// Receive state changes +zoomSdk.addEventListener('onMessage', (event) => { + const data = JSON.parse(event.payload); + applyChange(data); +}); +``` + +### Pattern 2: Server Relay + +Your backend is the source of truth: + +```javascript +const socket = io('https://your-server.com'); +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +socket.emit('join', { room: meetingUUID }); + +socket.on('state-update', (state) => renderApp(state)); +socket.emit('action', { type: 'vote', option: 'A' }); +``` + +### Pattern 3: CRDT (Conflict-Free) + +Best for concurrent editing (text, drawings): + +```javascript +import * as Y from 'yjs'; +import { WebrtcProvider } from 'y-webrtc'; + +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +const ydoc = new Y.Doc(); +const provider = new WebrtcProvider(meetingUUID, ydoc); +// Changes sync automatically via CRDT +``` + +## Meeting UUID as Room ID + +Use `getMeetingUUID()` as the room identifier for state synchronization: + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +// Same UUID for all participants in the same meeting/room +// Different UUID in breakout rooms +``` + +## Detailed Guides + +- **[Collaborate Mode Example](../../zoom-apps-sdk/examples/collaborate-mode.md)** - Complete implementation +- **[App Communication](../../zoom-apps-sdk/examples/app-communication.md)** - Instance messaging +- **[Breakout Rooms](../../zoom-apps-sdk/examples/breakout-rooms.md)** - Cross-room state sync +- **Sample app**: https://github.com/zoom/zoomapps-texteditor-vuejs + +## Skill Chain + +``` +zoom-apps-sdk (Collaborate + Communication) --> oauth (authorization) +``` diff --git a/partner-built/zoom-plugin/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md b/partner-built/zoom-plugin/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md new file mode 100644 index 00000000..95bc7d47 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md @@ -0,0 +1,36 @@ +# Contact Center App Lifecycle and Context Switching + +Build a Contact Center app that survives engagement switching without losing in-progress work. + +## Skills Needed + +- `contact-center` +- `zoom-apps-sdk` +- `zoom-oauth` (if backend identity mapping is required) + +## Problem + +Agents can switch between active engagements. Your app instance may remain alive while visible context changes. If state is global rather than engagement-scoped, data corruption and agent frustration follow. + +## Recommended Pattern + +1. Configure SDK with engagement capabilities. +2. Query initial context and status. +3. Subscribe to engagement context and status change events. +4. Store drafts and workflow state by `engagementId`. +5. On context switch, load the target engagement state. +6. On end state, finalize or clear that engagement data. + +## Failure Modes To Avoid + +- Single shared draft object for all engagements. +- Late event subscription after user interaction starts. +- Hard cleanup on tab switch instead of engagement end. +- Assuming visibility equals process lifetime. + +## Implementation References + +- `../../contact-center/web/examples/app-context-and-state.md` +- `../../contact-center/concepts/architecture-and-lifecycle.md` +- `../../contact-center/RUNBOOK.md` + diff --git a/partner-built/zoom-plugin/skills/general/use-cases/contact-center-integration.md b/partner-built/zoom-plugin/skills/general/use-cases/contact-center-integration.md new file mode 100644 index 00000000..7d1b112b --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/contact-center-integration.md @@ -0,0 +1,39 @@ +# Contact Center Integration + +Build support and engagement workflows with Zoom Contact Center across app, web, and mobile surfaces. + +## Skills Needed + +- `contact-center` (primary) +- `zoom-apps-sdk` (for Contact Center apps in Zoom client) +- `zoom-rest-api` (for Contact Center API automation) +- `zoom-oauth` (for authorization patterns) + +## Choose the Surface + +1. Contact Center app in Zoom client: +- Use engagement APIs/events and state by `engagementId`. +2. Website embed: +- Use campaign/web SDK scripts with readiness gating. +3. Native mobile: +- Use Android/iOS SDK service lifecycle patterns. + +## Core Architecture + +1. Initialize context and identity. +2. Start channel service (chat/video/ZVA/scheduled callback). +3. Handle engagement events and context switches. +4. Persist engagement-scoped workflow state. +5. End and cleanup channel services. + +## High-Value Use Cases + +- Agent notes app keyed by engagement. +- CRM integration with Smart Embed events. +- Campaign-driven routing to chat/video channels. +- Native app rejoin flow for dropped video sessions. + +## Where to Go Next + +- `../../contact-center/SKILL.md` +- `../../contact-center/RUNBOOK.md` diff --git a/partner-built/zoom-plugin/skills/general/use-cases/custom-meeting-ui-web.md b/partner-built/zoom-plugin/skills/general/use-cases/custom-meeting-ui-web.md new file mode 100644 index 00000000..b9e683cd --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/custom-meeting-ui-web.md @@ -0,0 +1,96 @@ +# Custom Meeting UI (Web) + +Build a custom video user interface around a real Zoom meeting in a web application. + +## Correct Skill Path + +- Primary skill: [../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md) +- Supporting auth guidance: [../../oauth/SKILL.md](../../oauth/SKILL.md) + +Do not route this use case to Video SDK unless the user is building a non-meeting custom +session product. + +## Why Component View + +Use Meeting SDK Component View when: +- the app must join a real Zoom meeting +- the meeting should render inside your page layout +- you need custom placement and styling around the Zoom meeting UI +- you still want Zoom meeting semantics such as real meeting join/start behavior + +Do not use Video SDK when: +- the requirement is specifically a Zoom meeting +- the user expects Meeting SDK auth, meeting numbers, passwords, ZAK/OBF rules, or webinar behavior + +## Minimal Architecture + +```text +Browser UI + -> fetch signature from backend + -> ZoomMtgEmbedded.createClient() + -> client.init({ zoomAppRoot }) + -> client.join({ signature, sdkKey, meetingNumber, userName, password }) +``` + +## Minimal Flow + +1. Create a backend signature endpoint. +2. In the browser, create one `ZoomMtgEmbedded` client instance. +3. Initialize it with a real DOM container. +4. Join using backend-generated signature plus meeting credentials. +5. Handle join/init errors explicitly in UI state. + +## Minimal Example + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +export async function joinEmbeddedMeeting({ + meetingNumber, + userName, + password, +}) { + const res = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + + if (!res.ok) { + throw new Error(`signature_fetch_failed:${res.status}`); + } + + const { signature, sdkKey } = await res.json(); + + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await client.join({ + signature, + sdkKey, + meetingNumber, + userName, + password, + }); +} +``` + +## Common Failure Points + +- Wrong route: using Video SDK instead of Meeting SDK Component View +- Missing backend signature generation +- Wrong password field name in the wrong view (`password` here, not `passWord`) +- Missing OBF/ZAK requirements for meetings outside the app account +- Missing SharedArrayBuffer headers when higher-end web meeting features are expected + +## References + +- [../../meeting-sdk/web/SKILL.md](../../meeting-sdk/web/SKILL.md) +- [../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md) +- [../../meeting-sdk/web/troubleshooting/error-codes.md](../../meeting-sdk/web/troubleshooting/error-codes.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/custom-video.md b/partner-built/zoom-plugin/skills/general/use-cases/custom-video.md new file mode 100644 index 00000000..ae0c0910 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/custom-video.md @@ -0,0 +1,232 @@ +# Custom Video + +Build fully branded video experiences with complete UI control. + +## Overview + +Use the Zoom Video SDK to create custom video applications with your own UI, branding, and user experience - powered by Zoom's infrastructure. + +## Skills Needed + +- **zoom-video-sdk** - Primary + +## Meeting SDK vs Video SDK + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| UI | Zoom's UI | Your custom UI | +| Branding | Zoom branding | Your branding | +| Experience | Zoom meetings | Video sessions | +| Features | Full Zoom features | Core video features | + +## Platform Options + +| Platform | Use Case | Guide | +|----------|----------|-------| +| Web | Browser-based custom video | [video-sdk/SKILL.md](../../video-sdk/SKILL.md) | +| Linux | Headless bots, raw media capture/injection | [video-sdk/linux/linux.md](../../video-sdk/linux/linux.md) | +| iOS | Custom video on iPhone/iPad | - | +| Android | Custom video on Android | - | +| Desktop | Custom desktop video apps | - | + +## Quick Start (Web) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'CDN'); +await client.join(topic, signature, userName, password); + +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +## Common Tasks + +### Building Custom Video Layouts + +```javascript +// Initialize Video SDK +const client = ZoomVideo.createClient(); +await client.init('en-US', 'CDN'); +await client.join(topic, signature, userName, password); + +const stream = client.getMediaStream(); + +// Start my video +await stream.startVideo(); +await stream.renderVideo( + document.querySelector('#my-video'), + client.getSessionInfo().userId, + 1280, 720, // width, height + 0, 0, // x, y offset + 3 // quality (1-4) +); + +// Listen for other participants +client.on('user-added', (payload) => { + payload.forEach(async (user) => { + if (user.bVideoOn) { + await renderParticipantVideo(user.userId); + } + }); +}); + +async function renderParticipantVideo(userId) { + const container = createVideoContainer(userId); + await stream.renderVideo(container, userId, 640, 360, 0, 0, 2); +} +``` + +### Gallery View Implementation + +```javascript +class GalleryView { + constructor(containerEl, maxPerPage = 25) { + this.container = containerEl; + this.maxPerPage = maxPerPage; + this.currentPage = 0; + this.participants = []; + } + + updateLayout() { + const count = Math.min(this.participants.length, this.maxPerPage); + const cols = Math.ceil(Math.sqrt(count)); + const rows = Math.ceil(count / cols); + + const cellWidth = this.container.clientWidth / cols; + const cellHeight = this.container.clientHeight / rows; + + // Render each participant in grid + this.participants.slice(0, this.maxPerPage).forEach((userId, index) => { + const x = (index % cols) * cellWidth; + const y = Math.floor(index / cols) * cellHeight; + + stream.renderVideo( + this.getCanvas(userId), + userId, + cellWidth, cellHeight, + x, y, 2 + ); + }); + } + + addParticipant(userId) { + this.participants.push(userId); + this.updateLayout(); + } + + removeParticipant(userId) { + this.participants = this.participants.filter(id => id !== userId); + this.updateLayout(); + } +} +``` + +### Speaker View Implementation + +```javascript +class SpeakerView { + constructor(mainEl, stripEl) { + this.mainVideo = mainEl; + this.stripContainer = stripEl; + this.activeSpeaker = null; + this.participants = []; + } + + setActiveSpeaker(userId) { + if (this.activeSpeaker === userId) return; + + this.activeSpeaker = userId; + + // Render active speaker large + stream.renderVideo( + this.mainVideo, + userId, + 1280, 720, 0, 0, 4 // High quality + ); + + // Update thumbnail strip + this.updateStrip(); + } + + updateStrip() { + const others = this.participants.filter(id => id !== this.activeSpeaker); + const thumbWidth = 160; + const thumbHeight = 90; + + others.forEach((userId, index) => { + stream.renderVideo( + this.getStripCanvas(userId), + userId, + thumbWidth, thumbHeight, + index * thumbWidth, 0, 1 // Lower quality + ); + }); + } +} + +// Auto-switch to active speaker +client.on('active-speaker', (payload) => { + if (payload.userId) { + speakerView.setActiveSpeaker(payload.userId); + } +}); +``` + +### Custom Controls + +```javascript +// Audio controls +async function toggleMute() { + const stream = client.getMediaStream(); + const isMuted = stream.isAudioMuted(); + + if (isMuted) { + await stream.unmuteAudio(); + } else { + await stream.muteAudio(); + } + + updateMuteButton(!isMuted); +} + +// Video controls +async function toggleVideo() { + const stream = client.getMediaStream(); + const isVideoOn = stream.isCapturingVideo(); + + if (isVideoOn) { + await stream.stopVideo(); + } else { + await stream.startVideo(); + await stream.renderVideo(myCanvas, myUserId, 1280, 720, 0, 0, 3); + } + + updateVideoButton(!isVideoOn); +} + +// Device selection +async function switchCamera(deviceId) { + const stream = client.getMediaStream(); + await stream.switchCamera(deviceId); +} + +async function switchMicrophone(deviceId) { + const stream = client.getMediaStream(); + await stream.switchMicrophone(deviceId); +} + +// Get available devices +const cameras = await ZoomVideo.getCameras(); +const mics = await ZoomVideo.getMicrophones(); +const speakers = await ZoomVideo.getSpeakers(); +``` + +## Resources + +- **Video SDK docs**: https://developers.zoom.us/docs/video-sdk/ +- **Web sample**: https://github.com/zoom/videosdk-web-sample +- **UI Toolkit**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/customer-support-cobrowsing.md b/partner-built/zoom-plugin/skills/general/use-cases/customer-support-cobrowsing.md new file mode 100644 index 00000000..00b1ece0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/customer-support-cobrowsing.md @@ -0,0 +1,453 @@ +# Customer Support Co-Browsing + +Enable real-time collaborative browsing between support agents and customers for efficient issue resolution and form completion assistance. + +## Use Case Overview + +**Problem**: Customers struggle to describe issues or complete complex forms, leading to long support calls and high frustration. + +**Solution**: Implement Zoom Cobrowse SDK to allow support agents to view and assist customers' browsers in real-time, with privacy controls for sensitive data. + +**Related Skills**: +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Contact Center Integration](contact-center-integration.md) + +## Architecture + +``` +┌──────────────────────┐ ┌──────────────────────┐ +│ Customer Browser │ │ Support Agent │ +│ • View form/page │◄───────►│ • View customer │ +│ • Share PIN │ Sync │ • Provide guidance │ +│ • Get assistance │ │ • Draw annotations │ +└──────────────────────┘ └──────────────────────┘ + │ │ + └────────────┬───────────────────┘ + ▼ + ┌──────────────────────┐ + │ Your Auth Server │ + │ • Generate JWTs │ + │ • Log sessions │ + │ • Track agents │ + └──────────────────────┘ +``` + +## Implementation Steps + +### 1. Set Up Authentication Server + +```javascript +// server.js - JWT token generation +const express = require('express'); +const { KJUR } = require('jsrsasign'); + +app.post('/cobrowse-token', (req, res) => { + const { role, userId, userName, caseId } = req.body; + + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 60 * 60 * 2; // 2 hours + + const payload = { + app_key: process.env.ZOOM_SDK_KEY, + role_type: role, // 1 = customer, 2 = agent + user_id: userId, + user_name: userName, + iat, + exp + }; + + const token = KJUR.jws.JWS.sign('HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); + + // Log session for tracking + logCobrowseSession(caseId, userId, role); + + res.json({ token }); +}); +``` + +### 2. Integrate Customer Support Page + +```html + + + + + + + +
+ + +
+ + +
+ + + + +
+ + + + +``` + +### 3. Agent Portal Integration + +```html + + + + + Support Agent - Co-Browse + + +
+
+

Case #

+

Customer:

+
+ + +
+ + + + +``` + +## Key Features + +### Privacy Masking + +Automatically hide sensitive customer data: + +```javascript +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask, .sensitive, [data-private]", + maskHTMLAttributes: "data-private=true" + } +}; +``` + +**What gets masked:** +- Social Security Numbers +- Credit Card Numbers +- Bank Account Numbers +- Passwords +- Any field with `.pii-mask` class + +### Annotation Tools + +Agents can guide customers visually: +- **Pen tool**: Highlight form fields +- **Rectangle**: Circle important sections +- **Pointer**: Direct attention to specific elements + +### Session Management + +Track and log all cobrowse sessions: + +```javascript +function logCobrowseSession(caseId, userId, role) { + const log = { + caseId, + userId, + role: role === 1 ? 'customer' : 'agent', + timestamp: new Date(), + sessionType: 'cobrowse' + }; + + database.sessions.insert(log); +} +``` + +## Use Case Scenarios + +### Scenario 1: Complex Form Assistance + +**Context**: Customer struggles with multi-step insurance application + +**Flow**: +1. Customer clicks "Get Help" on form page +2. PIN generated and displayed +3. Customer calls support, shares PIN +4. Agent enters PIN, sees customer's form +5. Agent uses annotation to highlight next field +6. Customer fills form with real-time guidance +7. Sensitive fields (SSN, medical info) masked from agent + +**Outcome**: Form completed in 5 minutes vs 20 minutes phone call + +### Scenario 2: Technical Troubleshooting + +**Context**: Customer can't find account settings + +**Flow**: +1. Customer initiates cobrowse from help chat +2. Agent joins session +3. Agent uses pointer to show navigation path +4. Customer follows visual guidance +5. Issue resolved in real-time + +**Outcome**: No screen sharing software needed, instant resolution + +### Scenario 3: Onboarding New Users + +**Context**: First-time user needs guided tour + +**Flow**: +1. Onboarding specialist starts cobrowse +2. Customer joins via PIN +3. Specialist guides through features +4. Annotations highlight key functions +5. Customer follows along in their browser + +**Outcome**: Interactive onboarding, higher completion rate + +## Security Considerations + +### 1. Privacy Compliance + +```javascript +// GDPR/CCPA compliant data handling +const privacySettings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask" + }, + sessionRecording: false, // Don't record sessions + dataRetention: "24h" // Auto-delete session logs +}; +``` + +### 2. Agent Authentication + +```javascript +// Verify agent credentials before token generation +async function validateAgent(agentId) { + const agent = await database.agents.findOne({ id: agentId }); + + if (!agent || !agent.cobrowseEnabled) { + throw new Error("Agent not authorized for cobrowse"); + } + + return agent; +} +``` + +### 3. Session Limits + +- Max 5 agents per customer session +- 2-hour token expiration +- Automatic session end on inactivity (10 minutes) +- HTTPS required for all connections + +## Metrics and Analytics + +Track cobrowse effectiveness: + +```javascript +// Track session metrics +const metrics = { + sessionDuration: calculateDuration(startTime, endTime), + issueResolved: true, + customerSatisfaction: 5, + formFieldsCompleted: 12, + annotationsUsed: 8 +}; + +analytics.track('cobrowse_session_completed', metrics); +``` + +**Key Metrics**: +- Average session duration +- Issue resolution rate +- Customer satisfaction (CSAT) +- Time saved vs phone support +- Conversion rate (form completion) + +## Integration with Existing Systems + +### Salesforce Integration + +```javascript +// Log cobrowse session to Salesforce case +async function logToSalesforce(caseId, sessionData) { + await salesforce.cases.update(caseId, { + Cobrowse_Session_Date__c: new Date(), + Cobrowse_PIN__c: sessionData.pin, + Agent_Id__c: sessionData.agentId, + Session_Duration__c: sessionData.duration + }); +} +``` + +### Zendesk Integration + +```javascript +// Add cobrowse note to Zendesk ticket +await zendesk.tickets.addComment(ticketId, { + body: `Cobrowse session completed. PIN: ${pin}. Duration: ${duration}`, + public: false +}); +``` + +## Best Practices + +1. **Clear Privacy Disclosure** + - Inform customer what agent can see + - Display "Agent is viewing your screen" banner + +2. **Selective Masking** + - Mask by default, unmask if needed + - Use `.pii-mask` class liberally + +3. **Session Logging** + - Track all sessions for compliance + - Log agent actions for quality assurance + +4. **Agent Training** + - Train agents on privacy controls + - Establish annotation best practices + +5. **Customer Consent** + - Get explicit consent before starting + - Allow customer to end session anytime + +## Cost Considerations + +**Zoom SDK Pricing**: +- Pay per cobrowse minute +- SDK Universal Credits required +- See [Zoom pricing](https://zoom.us/pricing/sdk) + +**Estimated Costs**: +- 100 sessions/day × 10 min avg = 1,000 minutes/day +- ~$30-50/day depending on plan + +## Related Resources + +- [Zoom Cobrowse SDK Documentation](../../cobrowse-sdk/SKILL.md) +- [Get Started Guide](../../cobrowse-sdk/get-started.md) +- [Session Events Reference](../../cobrowse-sdk/references/session-events.md) +- [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) +- [Contact Center Integration](contact-center-integration.md) + +## Common Issues + +**Issue**: Customer can't see PIN +**Solution**: Check `pincode_updated` event handler is properly attached + +**Issue**: Agent sees sensitive data +**Solution**: Verify `.pii-mask` class applied to sensitive fields + +**Issue**: Session disconnects on page refresh +**Solution**: Implement auto-reconnection pattern (see [Auto-Reconnection](../../cobrowse-sdk/examples/auto-reconnection.md)) + +## Next Steps + +1. Review [Get Started Guide](../../cobrowse-sdk/get-started.md) +2. Set up auth server using [JWT Authentication](../../cobrowse-sdk/concepts/jwt-authentication.md) +3. Integrate customer page with [Customer Integration](../../cobrowse-sdk/examples/customer-integration.md) +4. Deploy agent portal with [Agent Integration](../../cobrowse-sdk/examples/agent-integration.md) +5. Test privacy masking with [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/electron-meeting-embed.md b/partner-built/zoom-plugin/skills/general/use-cases/electron-meeting-embed.md new file mode 100644 index 00000000..91c2041f --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/electron-meeting-embed.md @@ -0,0 +1,28 @@ +# Electron Meeting Embed + +Use this flow when you need Zoom meetings embedded inside a desktop Electron application. + +## When to Use + +- You ship a desktop app with Electron. +- You need native-like meeting controls in app workflows. +- You need meeting modules beyond basic join/leave (recording, participants, share, raw data). + +## Skill Chain + +1. [meeting-sdk/electron](../../meeting-sdk/electron/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Meeting SDK JWT. +2. Electron app initializes SDK and authenticates. +3. App joins/starts meeting and binds controllers. +4. Optional advanced modules (raw data, webinar, whiteboard) are enabled as needed. +5. App leaves and performs explicit SDK cleanup. + +## References + +- [Electron Meeting SDK Skill](../../meeting-sdk/electron/SKILL.md) +- [Lifecycle Workflow](../../meeting-sdk/electron/concepts/lifecycle-workflow.md) +- [Deprecated and Contradictions](../../meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/embed-meetings.md b/partner-built/zoom-plugin/skills/general/use-cases/embed-meetings.md new file mode 100644 index 00000000..9cd6b943 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/embed-meetings.md @@ -0,0 +1,230 @@ +# Embed Meetings + +Embed the full Zoom meeting experience into your web or mobile application. + +## Overview + +Use the Zoom Meeting SDK to embed complete Zoom meetings into your application with Zoom's UI and features. + +## Skills Needed + +- **zoom-meeting-sdk** - Primary + +## Platform Options + +| Platform | View Options | +|----------|--------------| +| Web | Component View, Client View | +| iOS | Native SDK | +| Android | Native SDK | +| Desktop | Native SDK | + +## Web Views + +| View | Description | +|------|-------------| +| Component View | Extractable, customizable UI elements | +| Client View | Full-page Zoom meeting experience | + +## Quick Start (Web Component View) + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +client.join({ + sdkKey: 'YOUR_SDK_KEY', + signature: 'YOUR_SIGNATURE', + meetingNumber: 'MEETING_NUMBER', + userName: 'User Name', +}); +``` + +## Common Tasks + +### Component View Setup + +Component View embeds the meeting in a specific DOM element, allowing you to build UI around it. + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Create client +const client = ZoomMtgEmbedded.createClient(); + +// Initialize +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + customize: { + video: { + isResizable: true, + viewSizes: { + default: { width: 1000, height: 600 }, + ribbon: { width: 300, height: 700 } + } + }, + meetingInfo: ['topic', 'host', 'mn', 'pwd', 'telPwd', 'invite', 'participant', 'dc', 'enctype'], + toolbar: { + buttons: [ + { text: 'Custom', className: 'CustomButton', onClick: () => console.log('Custom clicked') } + ] + } + } +}); + +// Join meeting +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: '123456789', + password: 'password', + userName: 'John Doe', + userEmail: 'john@example.com' +}); +``` + +### Client View Setup + +Client View opens a full-page meeting experience (traditional Zoom UI). + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Load dependencies +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Initialize +ZoomMtg.init({ + leaveUrl: 'https://your-app.com/meeting-ended', + success: () => { + ZoomMtg.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: '123456789', + passWord: 'password', + userName: 'John Doe', + userEmail: 'john@example.com', + success: (res) => { + console.log('Joined meeting', res); + }, + error: (err) => { + console.error('Join error', err); + } + }); + } +}); +``` + +### Customizing Meeting UI + +```javascript +// Hide specific UI elements +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + customize: { + // Hide meeting info + meetingInfo: [], + + // Customize toolbar + toolbar: { + buttons: [ + // Add custom buttons + { + text: 'Info', + className: 'info-btn', + onClick: () => showInfo() + } + ] + }, + + // Video layout + video: { + viewSizes: { + default: { width: 1280, height: 720 } + }, + popper: { + disableDraggable: false + } + }, + + // Active speaker view + activeStateEnabledMode: { + enabled: true + } + } +}); + +// Change view programmatically +client.changeView('gallery'); // 'speaker' | 'gallery' | 'ribbon' +``` + +### Handling Meeting Events + +```javascript +// Meeting status events +client.on('connection-change', (payload) => { + const { state } = payload; + switch (state) { + case 'Connected': + console.log('Connected to meeting'); + break; + case 'Reconnecting': + console.log('Reconnecting...'); + break; + case 'Closed': + console.log('Meeting ended'); + handleMeetingEnd(); + break; + } +}); + +// User events +client.on('user-added', (payload) => { + console.log('User joined:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('User left:', payload); +}); + +// Audio/video events +client.on('active-speaker', (payload) => { + console.log('Active speaker:', payload.userId); +}); + +// Error handling +client.on('error', (payload) => { + console.error('SDK Error:', payload); +}); +``` + +### Leave/End Meeting + +```javascript +// Leave meeting (participant) +client.leaveMeeting(); + +// End meeting (host only) +client.endMeeting(); + +// Handle leave URL (Client View) +// User is redirected to leaveUrl specified in init() +``` + +## Native SDK (iOS/Android) + +For native mobile apps, see: +- [Meeting SDK iOS](../../meeting-sdk/references/ios.md) +- [Meeting SDK Android](../../meeting-sdk/references/android.md) + +## Resources + +- **Meeting SDK docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Web sample**: https://github.com/zoom/meetingsdk-web-sample +- **Component View docs**: https://developers.zoom.us/docs/meeting-sdk/web/component-view/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/enterprise-app-deployment.md b/partner-built/zoom-plugin/skills/general/use-cases/enterprise-app-deployment.md new file mode 100644 index 00000000..4da03b7c --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/enterprise-app-deployment.md @@ -0,0 +1,34 @@ +# Enterprise App Deployment (Account-Wide Install / Approval) + +High-frequency questions in the forum clusters: + +- "How do I deploy an app to an entire company?" +- "How do I allow other users to use my app?" +- "Why can only the admin use it?" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-general** | Understand Marketplace deployment/approval concepts | +| 2 | **zoom-rest-api** (optional) | Automate admin tasks (where supported) | + +## What To Clarify Upfront + +- Is this an internal app (single account) or public Marketplace app? +- Does the customer want: + - admin pre-approval + user install? + - account-level installation? + - restricting installs to specific users/groups? + +## Common Fix Patterns + +- Ensure the app is configured for the right audience and install flow. +- Ensure required scopes are approved and users re-consented if scopes changed. +- If users are blocked, check account admin Marketplace policies. + +## Links + +- `marketplace-publishing.md` +- `../references/marketplace.md` + diff --git a/partner-built/zoom-plugin/skills/general/use-cases/flutter-video-sessions.md b/partner-built/zoom-plugin/skills/general/use-cases/flutter-video-sessions.md new file mode 100644 index 00000000..00f11fc0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/flutter-video-sessions.md @@ -0,0 +1,27 @@ +# Flutter Video Sessions + +Use this flow when you are building custom real-time video sessions in a Flutter mobile app. + +## When to Use + +- You need full control over UI/UX (not Zoom Meeting UI). +- You are building iOS/Android mobile session experiences in Flutter. +- You need helper-driven features such as chat, share, recording, or transcription. + +## Skill Chain + +1. [video-sdk/flutter](../../video-sdk/flutter/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Video SDK JWT. +2. Flutter app initializes SDK and binds event listeners. +3. App joins session and activates media/helpers. +4. App leaves and cleans up explicitly. + +## References + +- [Flutter Video SDK Skill](../../video-sdk/flutter/SKILL.md) +- [Lifecycle Workflow](../../video-sdk/flutter/concepts/lifecycle-workflow.md) +- [Session Join Pattern](../../video-sdk/flutter/examples/session-join-pattern.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/form-completion-assistant.md b/partner-built/zoom-plugin/skills/general/use-cases/form-completion-assistant.md new file mode 100644 index 00000000..89d74350 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/form-completion-assistant.md @@ -0,0 +1,527 @@ +# Form Completion Assistant with Co-Browsing + +Guide customers through complex forms in real-time using visual assistance and privacy-protected co-browsing. + +## Use Case Overview + +**Problem**: High form abandonment rates due to complexity, confusion, or lack of confidence in data entry. + +**Solution**: Implement Zoom Cobrowse SDK to provide real-time visual guidance while protecting sensitive customer data through privacy masking. + +**Related Skills**: +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Customer Support Co-Browsing](customer-support-cobrowsing.md) + +## Target Scenarios + +### Financial Services +- Loan applications +- Account opening forms +- Investment questionnaires +- Insurance claims + +### Healthcare +- Patient intake forms +- Insurance enrollment +- Medical history questionnaires +- Telehealth registration + +### E-Commerce +- Complex checkout flows +- B2B order forms +- Subscription sign-ups +- Custom product configurators + +## Implementation + +### Quick Start Pattern + +```html + + + + Loan Application - Form Assistant + + + +
+

Loan Application

+ +
+ +
+ +
+

Personal Information

+ + + + +
+ + +
+

Identification

+ + 🔒 This field is hidden from support agents + + + 🔒 This field is hidden from support agents +
+ + +
+

Financial Information

+ + + + + 🔒 This field is hidden from support agents + + + 🔒 This field is hidden from support agents +
+ + +
+

Loan Request

+ + +
+
+ + + + + + +``` + +## Key Features + +### Progressive Privacy Masking + +Mask fields based on sensitivity level: + +```javascript +// Tier 1: Fully masked (never visible to agent) +// - SSN, passwords, credit cards +const tier1Fields = ".pii-mask, [data-privacy='full']"; + +// Tier 2: Masked by default, can be unmasked with consent +// - Bank account numbers, tax IDs +const tier2Fields = "[data-privacy='optional']"; + +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: tier1Fields, + } +}; +``` + +### Smart Annotation Guidance + +Agent can highlight and guide: + +```html + + +``` + +### Real-Time Validation Assistance + +Agent sees validation errors in real-time: + +```javascript +// Show validation status to agent +form.addEventListener("blur", (e) => { + if (e.target.validity.valid) { + e.target.classList.add("valid"); + } else { + e.target.classList.add("invalid"); + // Agent can see this and provide guidance + } +}, true); +``` + +## Workflow Example + +### Multi-Step Form Assistance + +``` +CUSTOMER AGENT + │ │ + │ 1. Opens loan application │ + │ (Step 1 of 4) │ + │ │ + │ 2. Confused at Step 2 │ + │ Clicks "Need Help?" │ + ├────────► Request Help │ + │ (PIN: 123456) │ + │ │ + │ 3. Calls support │ + │ "I need help with │ + │ loan application" │ + │ │ + │ │ 4. Agent opens case + │ │ Enters PIN: 123456 + │ │ + │ ◄─────── Agent Joined ─────────┤ + │ │ + │ 5. Agent sees form (Step 2) │ + │ Masked fields: SSN, DOB │ + │ Visible: Everything else │ + │ │ + │ │ 6. Agent uses pen tool + │ ◄────── Highlight field ───────┤ Highlights "SSN" + │ │ Says: "Enter your SSN" + │ │ + │ 7. Enters SSN │ + │ (Agent sees: ***-**-****) │ + │ │ + │ │ 8. Agent highlights next + │ ◄────── Highlight DOB ─────────┤ "Now your date of birth" + │ │ + │ 9. Completes Step 2 │ + │ Moves to Step 3 │ + │ │ + │ 10. Finishes form │ + │ ─────► Form Submitted ─────────► + │ │ + │ 11. Thanks agent │ + │ ◄────── Session Ends ──────────┤ +``` + +## Privacy Protection Patterns + +### Pattern 1: Full Masking (Recommended) + +```javascript +// All sensitive fields completely hidden +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask, .sensitive, [data-private]" + } +}; +``` + +**Use for**: SSN, passwords, credit cards, medical records + +### Pattern 2: Partial Masking + +```javascript +// Show last 4 digits (not natively supported, requires custom implementation) +function partialMask(value) { + return '*'.repeat(value.length - 4) + value.slice(-4); +} +``` + +**Use for**: Phone numbers (show area code), account numbers + +### Pattern 3: Contextual Masking + +```javascript +// Mask based on form section +function updateMasking(stepNumber) { + const maskingRules = { + 1: ".none", // No masking on basic info + 2: ".ssn, .dob", // Mask ID fields + 3: ".account, .routing", // Mask financial fields + 4: ".none" // No masking on loan details + }; + + // Update SDK settings dynamically + // Note: Requires re-initialization in current SDK version +} +``` + +## Analytics and Tracking + +### Measure Assistance Effectiveness + +```javascript +// Track form completion with/without assistance +const metrics = { + formType: "loan_application", + assistanceRequested: true, + assistanceDuration: 420, // seconds + stepWhereHelpRequested: 2, + completionRate: 1, // 1 = completed, 0 = abandoned + timeToComplete: 1200, // seconds + fieldsModified: 18, + validationErrors: 2 +}; + +analytics.track("form_completion", metrics); +``` + +### Key Metrics to Track + +1. **Form Abandonment Rate** + - Before assistance: 35% + - With assistance: 8% + - **73% improvement** + +2. **Time to Complete** + - Without assistance: 15 min avg + - With assistance: 8 min avg + - **47% faster** + +3. **Error Rate** + - Without assistance: 3.2 errors/form + - With assistance: 0.8 errors/form + - **75% fewer errors** + +4. **Customer Satisfaction** + - CSAT score: 4.7/5 + - NPS: +68 + +## Integration Examples + +### Salesforce Integration + +```javascript +// Log form assistance session to Salesforce +async function logFormAssistance(leadId, sessionData) { + await salesforce.leads.update(leadId, { + Form_Assistance_Date__c: new Date(), + Assistance_Duration__c: sessionData.duration, + Form_Completed__c: sessionData.completed, + Agent_Id__c: sessionData.agentId + }); +} +``` + +### HubSpot Integration + +```javascript +// Create activity for form assistance +await hubspot.contacts.createActivity(contactId, { + type: "cobrowse_assistance", + timestamp: Date.now(), + properties: { + form_type: "loan_application", + duration: sessionDuration, + completed: formCompleted + } +}); +``` + +## Best Practices + +### 1. Clear Help Triggers + +Place help buttons strategically: +- Top of form (always visible) +- Beginning of each complex section +- Near confusing field labels + +### 2. Privacy First + +- Mask by default, unmask only if absolutely necessary +- Show clear indicators when fields are masked +- Get consent before starting session + +### 3. Progress Preservation + +```javascript +// Save form state before assistance +function saveFormState() { + const formData = new FormData(document.getElementById("loan-application")); + localStorage.setItem("form_draft", JSON.stringify(Object.fromEntries(formData))); +} + +// Restore on page reload +function restoreFormState() { + const saved = localStorage.getItem("form_draft"); + if (saved) { + const data = JSON.parse(saved); + Object.entries(data).forEach(([name, value]) => { + const field = document.querySelector(`[name="${name}"]`); + if (field) field.value = value; + }); + } +} +``` + +### 4. Agent Training + +Train agents on: +- Which fields are masked vs visible +- How to use annotation tools effectively +- When to suggest breaks for complex forms +- Privacy compliance requirements + +## Common Challenges & Solutions + +**Challenge**: Customer refreshes page during assistance +**Solution**: Implement auto-reconnection (see [Auto-Reconnection Guide](../../cobrowse-sdk/examples/auto-reconnection.md)) + +**Challenge**: Agent can't see validation errors +**Solution**: Add visual indicators that aren't masked: +```javascript +field.parentElement.classList.add("has-error"); +``` + +**Challenge**: Multi-page forms lose session +**Solution**: Use session persistence across page navigation: +```javascript +const settings = { + multiTabSessionPersistence: { + enable: true + } +}; +``` + +## Cost-Benefit Analysis + +### Costs +- **Zoom SDK**: ~$0.05 per assistance minute +- **Development**: 2-3 weeks integration +- **Agent Training**: 4 hours per agent + +### Benefits +- **Reduced Abandonment**: 35% → 8% = 27% more conversions +- **Faster Completion**: 15 min → 8 min = 47% time saved +- **Fewer Errors**: 75% reduction in submission errors +- **Higher CSAT**: 4.7/5 satisfaction score + +**ROI Example** (1000 forms/month): +- Before: 650 completed (35% abandoned) +- After: 920 completed (8% abandoned) +- Additional conversions: 270/month +- Avg value/conversion: $50 +- Additional revenue: $13,500/month +- SDK cost: ~$500/month +- **Net benefit: $13,000/month** + +## Related Resources + +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Customer Support Co-Browsing](customer-support-cobrowsing.md) +- [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) +- [Session Events Reference](../../cobrowse-sdk/references/session-events.md) +- [Get Started Guide](../../cobrowse-sdk/get-started.md) + +## Next Steps + +1. **Identify high-abandon forms** in your application +2. **Review privacy requirements** for your industry +3. **Set up auth server** using [JWT Authentication](../../cobrowse-sdk/concepts/jwt-authentication.md) +4. **Integrate customer SDK** using [Customer Integration](../../cobrowse-sdk/examples/customer-integration.md) +5. **Train support agents** on co-browsing and privacy controls +6. **Measure results** and iterate based on analytics diff --git a/partner-built/zoom-plugin/skills/general/use-cases/hd-video-resolution.md b/partner-built/zoom-plugin/skills/general/use-cases/hd-video-resolution.md new file mode 100644 index 00000000..a39dc788 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/hd-video-resolution.md @@ -0,0 +1,336 @@ +# HD Video Resolution + +Achieve 720p and 1080p video quality in Zoom Web SDKs, including viewport size requirements that affect resolution. + +## Overview + +HD video quality in Zoom SDKs depends on multiple factors: container/viewport size, network bandwidth, SharedArrayBuffer support, and concurrent stream limits. **Video automatically scales down if the container is smaller than required dimensions.** + +## Skills Needed + +- **zoom-meeting-sdk** (Web) +- **zoom-video-sdk** (Web) + +## Viewport Size Requirements + +**Critical:** Video resolution is automatically adjusted based on the rendered container size. + +### Resolution Thresholds + +| Target Resolution | Minimum Container Size | Bandwidth Required | +|-------------------|------------------------|-------------------| +| **360p** | 480 × 270 | 600 kbps | +| **720p** | 1280 × 720 (or 720 × 411 gallery) | 1.2-1.5 Mbps | +| **1080p** | 1920 × 1080 | 2.5-3.0 Mbps | + +**If your video container is smaller than 1280×720, you will NOT get 720p video - it will automatically scale down.** + +### Meeting SDK Component View Constraints + +| View Type | Minimum | Maximum | Aspect Ratio | +|-----------|---------|---------|--------------| +| **Speaker** | 240 × 135 | 1440 × 810 | 16:9 | +| **Gallery** | 720 × 411 | 1440 × 720 | 16:9 | +| **Ribbon** | 240 × 135 | 316 × 720 | Variable | + +### Recommended Sizes for HD + +```javascript +// For 720p in speaker view +const speakerContainer = { + width: 1280, + height: 720 +}; + +// For 720p in gallery view (minimum) +const galleryContainer = { + width: 720, + height: 411 +}; + +// For 1080p (speaker view only) +const fullHDContainer = { + width: 1920, + height: 1080 +}; +``` + +## Concurrent Stream Limits + +**Video SDK enforces strict concurrent HD limits:** + +| Resolution | Concurrent Limit | Notes | +|------------|------------------|-------| +| **720p** | **Max 2 streams** | Attempting 3rd results in `Errors_Wrong_Usage` | +| **1080p** | **Only 1 stream** | Only one 1080p video can be rendered at a time | + +### Recommended Quality by View + +| View Type | Active Speaker | Other Participants | +|-----------|----------------|-------------------| +| **Speaker View** | 720p or 1080p | 180p | +| **Gallery (3×3)** | 360p all | 360p | +| **Gallery (5×5)** | 180p all | 180p | +| **Small thumbnails** | 180p | 180p | + +## SharedArrayBuffer (SAB) Requirement + +### Features Requiring SAB + +| Feature | SAB Required | +|---------|--------------| +| **Sending 720p video** | ✅ Yes | +| **Virtual Background** | ✅ Yes | +| **Gallery view (multiple videos)** | ✅ Yes | +| **Background noise suppression** | ✅ Yes | + +### Enabling SharedArrayBuffer + +Requires Cross-Origin Isolation headers on your server: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +**Express.js example:** +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +**Nginx example:** +```nginx +add_header Cross-Origin-Opener-Policy same-origin; +add_header Cross-Origin-Embedder-Policy require-corp; +``` + +### Browser Support for SAB + +| Browser | Minimum Version | +|---------|-----------------| +| Chrome | 68+ | +| Edge | 79+ | +| Firefox | 79+ | +| Safari | 15.2+ (macOS), iOS 15.2+ | +| Opera | 73+ | + +**Note:** Safari requires newer versions and may have limitations. + +### Impact of Missing SAB + +Without SharedArrayBuffer: +- Max sending resolution: **360p** +- No virtual background +- Limited to single video rendering +- No background noise suppression + +## Video SDK Configuration + +### Enable HD Video Capture + +```javascript +// Start video with HD enabled +await stream.startVideo({ + hd: true, // Enable 720p + fullHd: true, // Enable 1080p (if supported) + captureWidth: 1280, + captureHeight: 720 +}); +``` + +### Subscribe to Specific Quality + +```javascript +// VideoQuality enum values: +// Video_90P = 0 +// Video_180P = 1 +// Video_360P = 2 +// Video_720P = 3 +// Video_1080P = 4 + +// Attach video at specific quality +await stream.attachVideo(userId, VideoQuality.Video_720P); + +// Or with renderVideo +await stream.renderVideo(canvas, userId, 1280, 720, 0, 0, VideoQuality.Video_720P); +``` + +### Check Device Capabilities + +```javascript +// Check if device supports HD +const capabilities = await stream.getVideoCapabilities(); +console.log('Max resolution:', capabilities.maxResolution); +console.log('HD supported:', capabilities.hdSupported); +``` + +## Meeting SDK Configuration + +### Component View HD + +```javascript +ZoomMtg.init({ + leaveUrl: 'https://your-site.com', + disablePreview: false, + videoResolution: '720p', // or '1080p' + success: () => { + console.log('Init success'); + } +}); +``` + +### Responsive Container Setup + +```html +
+ +
+``` + +```javascript +// Ensure container meets minimum size for HD +const container = document.getElementById('zoom-container'); +const rect = container.getBoundingClientRect(); + +if (rect.width < 1280 || rect.height < 720) { + console.warn('Container too small for 720p - video will be downscaled'); +} +``` + +## WebRTC vs WebAssembly Mode + +| Mode | Characteristics | +|------|-----------------| +| **WebRTC** (Primary, SDK v2+) | Enhanced performance, adaptive bitrate, better congestion control | +| **WebAssembly** (Fallback) | Custom Zoom codec, more reliable 720p, supports virtual backgrounds | + +SDK v2 automatically selects mode based on network and device conditions. + +## Account Requirements (Zoom Meetings) + +For **Group HD** in Zoom Meetings (not SDK): + +| Requirement | 720p | 1080p | +|-------------|------|-------| +| Max video participants | 2 | 2 | +| Full-screen mode | Required | Required | +| Active speaker mode | Required | Required | +| CPU | Minimum specs | i7 Quad Core+ | +| Bandwidth | 1.5 Mbps | 3.0 Mbps | +| Mobile support | ❌ No | ❌ No | + +**Key:** If a third participant turns video on, quality reverts to standard definition. + +## Best Practices + +### 1. Size Your Container Correctly + +```javascript +function ensureHDContainer(container, targetResolution = 720) { + const minWidth = targetResolution === 1080 ? 1920 : 1280; + const minHeight = targetResolution === 1080 ? 1080 : 720; + + container.style.minWidth = `${minWidth}px`; + container.style.minHeight = `${minHeight}px`; + container.style.aspectRatio = '16/9'; +} +``` + +### 2. Handle Window Resize + +```javascript +window.addEventListener('resize', () => { + const container = document.getElementById('zoom-container'); + const rect = container.getBoundingClientRect(); + + // Adjust quality based on available space + if (rect.width >= 1920 && rect.height >= 1080) { + stream.attachVideo(userId, VideoQuality.Video_1080P); + } else if (rect.width >= 1280 && rect.height >= 720) { + stream.attachVideo(userId, VideoQuality.Video_720P); + } else { + stream.attachVideo(userId, VideoQuality.Video_360P); + } +}); +``` + +### 3. Check SAB Support + +```javascript +function checkSABSupport() { + if (typeof SharedArrayBuffer === 'undefined') { + console.warn('SharedArrayBuffer not available - HD features limited'); + return false; + } + + // Check if cross-origin isolated + if (!crossOriginIsolated) { + console.warn('Not cross-origin isolated - SAB may not work'); + return false; + } + + return true; +} +``` + +### 4. Limit Concurrent HD Streams + +```javascript +const MAX_720P_STREAMS = 2; +let current720pCount = 0; + +async function subscribeToVideo(userId, preferredQuality) { + let quality = preferredQuality; + + if (quality === VideoQuality.Video_720P) { + if (current720pCount >= MAX_720P_STREAMS) { + console.warn('Max 720p streams reached, using 360p'); + quality = VideoQuality.Video_360P; + } else { + current720pCount++; + } + } + + await stream.attachVideo(userId, quality); +} +``` + +### 5. Maintain 16:9 Aspect Ratio + +```css +.video-container { + position: relative; + width: 100%; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ +} + +.video-container canvas, +.video-container video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +``` + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Video stuck at 360p | Container too small | Resize container to ≥1280×720 | +| Video stuck at 360p | Missing SAB headers | Add COOP/COEP headers | +| 720p works, 1080p doesn't | Only one 1080p allowed | Check concurrent streams | +| HD works in dev, not prod | Different CORS headers | Verify production headers | +| Safari HD not working | SAB not supported | Check Safari version ≥15.2 | + +## Resources + +- **Video SDK HD docs**: https://developers.zoom.us/docs/video-sdk/web/video-hd/ +- **Meeting SDK resizing**: https://developers.zoom.us/docs/meeting-sdk/web/component-view/resizing/ +- **SharedArrayBuffer docs**: https://developers.zoom.us/docs/meeting-sdk/web/sharedarraybuffer/ +- **Cross-Origin Isolation**: https://web.dev/articles/coop-coep/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/high-volume-meeting-platform.md b/partner-built/zoom-plugin/skills/general/use-cases/high-volume-meeting-platform.md new file mode 100644 index 00000000..bbe92e9e --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/high-volume-meeting-platform.md @@ -0,0 +1,56 @@ +# High-Volume Meeting Platform + +Design a distributed system that creates large numbers of meetings and keeps meeting state accurate under retries, webhook delay, and partial outages. + +## Skills Needed + +- Primary: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Events: [../../webhooks/SKILL.md](../../webhooks/SKILL.md) +- Auth/token broker: [../../oauth/SKILL.md](../../oauth/SKILL.md) +- Deep implementation reference: [../references/distributed-meeting-fallback-architecture.md](../references/distributed-meeting-fallback-architecture.md) + +## Architecture Summary + +```text +API Gateway + -> Command Service + -> Idempotency Store + -> Token Broker + -> Zoom REST API + -> Outbox / Queue + +Webhook Ingress + -> Signature Verification + -> Durable Queue + -> Projection Workers + -> Meeting State Store + +Recovery + -> Retry Workers + -> Reconciliation Poller + -> DLQ Replay +``` + +## Core Rules + +1. Keep command and event planes separate. +2. Require idempotency keys on all meeting-creation requests. +3. Queue everything that touches Zoom when you need backpressure control. +4. Verify webhook signatures before durable write. +5. Reconcile state from REST when event delivery is delayed or incomplete. + +## Concrete Fallbacks + +- `429` / `5xx` from Zoom REST API -> retry with jitter, then queue for delayed retry. +- Token refresh contention -> single token broker refresh under distributed lock. +- Webhook processor outage -> accept only after queue write, use DLQ replay. +- Missing lifecycle events -> scheduled reconciliation poller repairs the projection. +- Downstream Zoom outage -> circuit breaker opens and create commands stay queued. + +## When to Use This + +Use this pattern when: +- multiple workers or services create meetings concurrently +- you need durable event processing +- missed webhook events are unacceptable +- you need a degraded-but-safe mode during Zoom or network instability diff --git a/partner-built/zoom-plugin/skills/general/use-cases/immersive-experiences.md b/partner-built/zoom-plugin/skills/general/use-cases/immersive-experiences.md new file mode 100644 index 00000000..d64578db --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/immersive-experiences.md @@ -0,0 +1,83 @@ +# Immersive Experiences + +Custom video layouts replacing the standard Zoom gallery view using the Layers API. + +## Overview + +The Layers API lets you take over the meeting's video display to create custom layouts - podcast formats, talk shows, classrooms, game shows, and branded meeting experiences. + +## Skills Needed + +- **zoom-apps-sdk** (Layers API) - Primary +- **oauth** - Authentication + +## Use Case Patterns + +| Pattern | Description | Key APIs | +|---------|-------------|----------| +| Podcast layout | 2-3 hosts with custom background | drawParticipant, drawImage | +| Talk show | Large host + row of guests | drawParticipant, drawImage | +| Classroom | Teacher prominent + student thumbnails | drawParticipant, drawImage | +| Game show | Custom positions with animated overlays | drawParticipant, drawImage, drawWebView | +| Branded meeting | Company background + participant positions | drawParticipant, drawImage | +| Interactive dashboard | Participants + live data panels | drawParticipant, drawWebView | + +## Architecture + +``` +Host Participants +──── ──────────── +Controls layout (UI panel) --> Receive layout via Socket.io/backend +runRenderingContext() --> runRenderingContext() +drawParticipant() x N --> drawParticipant() x N (same positions) +drawImage() (background) --> drawImage() (same background) +``` + +All participants must be in immersive mode and rendering the same layout. The host typically controls layout changes and broadcasts them via your backend (Socket.io, WebSocket). + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'getMeetingParticipants', 'onParticipantChange' + ], + version: '0.16' +}); + +// Start immersive mode +await zoomSdk.runRenderingContext({ view: 'immersive' }); + +// Get participants and layout them +const { participants } = await zoomSdk.getMeetingParticipants(); +// ... position participants with drawParticipant() +``` + +## Performance Considerations + +- Pre-render backgrounds to a single canvas image +- Minimize drawImage calls during animations +- Use requestAnimationFrame for smooth transitions +- Test on lower-end hardware (not everyone has a fast machine) +- Keep zIndex values in 0-10 range + +## Detailed Guides + +- **[Layers Immersive Example](../../zoom-apps-sdk/examples/layers-immersive.md)** - Complete podcast layout code +- **[Layers API Reference](../../zoom-apps-sdk/references/layers-api.md)** - All drawing methods +- **[Camera Mode Example](../../zoom-apps-sdk/examples/layers-camera.md)** - Virtual camera overlays +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js + +## Skill Chain + +``` +zoom-apps-sdk (Layers API) --> oauth (authorization) + | + v +zoom-rest-api (optional: meeting management) +``` diff --git a/partner-built/zoom-plugin/skills/general/use-cases/in-meeting-apps.md b/partner-built/zoom-plugin/skills/general/use-cases/in-meeting-apps.md new file mode 100644 index 00000000..a9932e42 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/in-meeting-apps.md @@ -0,0 +1,306 @@ +# In-Meeting Apps + +Build apps that run inside the Zoom client during meetings. + +## Overview + +Create Zoom Apps that appear within the Zoom meeting interface - polls, games, collaboration tools, and more that participants can interact with during meetings. + +## Skills Needed + +- **zoom-apps-sdk** - Primary +- **oauth** - Authentication +- **zoom-rest-api** - Server-side API calls (optional) + +## App Types + +| Type | Description | Key APIs | +|------|-------------|----------| +| Sidebar app | Panel alongside meeting | getMeetingContext, shareApp | +| Immersive app | Full-screen Layers API | runRenderingContext, drawParticipant | +| Camera mode | Virtual camera overlay | runRenderingContext({ view: 'camera' }) | +| Collaborate | Shared state app | startCollaborate, connect, postMessage | +| Background app | Runs without visible UI | Events, REST API calls | + +## Architecture + +``` +Frontend (Zoom embedded browser) Backend (Express/Node.js) +───────────────────────────────── ──────────────────────── +@zoom/appssdk OAuth token exchange +zoomSdk.config() REST API calls +zoomSdk.getMeetingContext() Token storage (Redis) +fetch('/api/data') ─────────────> Business logic +``` + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['shareApp', 'getMeetingContext', 'getUserContext'], + version: '0.16' +}); + +const context = await zoomSdk.getMeetingContext(); +console.log('Meeting ID:', context.meetingID); + +await zoomSdk.shareApp(); +``` + +## Common Tasks + +### Building a Poll App + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Initialize +await zoomSdk.config({ + capabilities: [ + 'shareApp', + 'getMeetingContext', + 'getMeetingParticipants', + 'sendAppInvitation' + ] +}); + +// Poll state +let currentPoll = { + question: '', + options: [], + votes: {} +}; + +// Create poll +function createPoll(question, options) { + currentPoll = { + question, + options, + votes: {} + }; + broadcastPollState(); +} + +// Submit vote +async function submitVote(optionIndex) { + const context = await zoomSdk.getMeetingContext(); + currentPoll.votes[context.participantId] = optionIndex; + broadcastPollState(); +} + +// Share results +function getResults() { + const counts = currentPoll.options.map((_, i) => + Object.values(currentPoll.votes).filter(v => v === i).length + ); + return currentPoll.options.map((opt, i) => ({ + option: opt, + count: counts[i], + percentage: (counts[i] / Object.keys(currentPoll.votes).length * 100).toFixed(1) + })); +} + +// Invite others to participate +async function inviteParticipants() { + await zoomSdk.sendAppInvitation({ + action: 'open', + message: 'Join the poll!' + }); +} +``` + +### Collaborative Whiteboard + +```javascript +// Whiteboard with real-time sync +const canvas = document.getElementById('whiteboard'); +const ctx = canvas.getContext('2d'); + +// Drawing state +let isDrawing = false; +let lastX = 0; +let lastY = 0; + +canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + [lastX, lastY] = [e.offsetX, e.offsetY]; +}); + +canvas.addEventListener('mousemove', (e) => { + if (!isDrawing) return; + + const stroke = { + from: { x: lastX, y: lastY }, + to: { x: e.offsetX, y: e.offsetY }, + color: currentColor, + width: currentWidth + }; + + drawStroke(stroke); + broadcastStroke(stroke); // Sync with others + + [lastX, lastY] = [e.offsetX, e.offsetY]; +}); + +function drawStroke(stroke) { + ctx.beginPath(); + ctx.moveTo(stroke.from.x, stroke.from.y); + ctx.lineTo(stroke.to.x, stroke.to.y); + ctx.strokeStyle = stroke.color; + ctx.lineWidth = stroke.width; + ctx.lineCap = 'round'; + ctx.stroke(); +} + +// Receive strokes from others +onRemoteStroke((stroke) => { + drawStroke(stroke); +}); +``` + +### Meeting Timer/Agenda + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Timer app +class MeetingTimer { + constructor() { + this.agenda = []; + this.currentItem = 0; + this.startTime = null; + } + + async init() { + await zoomSdk.config({ + capabilities: ['getMeetingContext', 'shareApp'] + }); + } + + setAgenda(items) { + // items: [{ title: 'Intro', duration: 5 }, ...] + this.agenda = items.map(item => ({ + ...item, + elapsed: 0, + status: 'pending' + })); + this.broadcastState(); + } + + start() { + this.startTime = Date.now(); + this.agenda[this.currentItem].status = 'active'; + this.tick(); + } + + tick() { + const item = this.agenda[this.currentItem]; + const elapsed = Math.floor((Date.now() - this.startTime) / 1000 / 60); + item.elapsed = elapsed; + + if (elapsed >= item.duration) { + this.alertTimeUp(); + } + + this.broadcastState(); + setTimeout(() => this.tick(), 1000); + } + + nextItem() { + this.agenda[this.currentItem].status = 'completed'; + this.currentItem++; + if (this.currentItem < this.agenda.length) { + this.startTime = Date.now(); + this.agenda[this.currentItem].status = 'active'; + } + } + + alertTimeUp() { + // Visual/audio alert + document.getElementById('timer').classList.add('warning'); + } +} +``` + +### Layers API Visuals + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Layers API for immersive experiences +await zoomSdk.config({ + capabilities: ['runRenderingContext', 'clearRenderingContext'] +}); + +// Start Layers mode +await zoomSdk.runRenderingContext({ + view: 'immersive' +}); + +// Draw on the video layer +const canvas = document.getElementById('layers-canvas'); +const ctx = canvas.getContext('2d'); + +// Example: Add participant name labels +function drawNameLabel(participant, x, y) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(x, y - 25, 150, 25); + ctx.fillStyle = 'white'; + ctx.font = '14px Arial'; + ctx.fillText(participant.name, x + 5, y - 8); +} + +// Example: Add virtual background effects +function drawVirtualEffect() { + // Draw confetti, borders, icons, etc. + // These overlay on top of video +} + +// Stop Layers mode +async function exitLayers() { + await zoomSdk.clearRenderingContext(); +} +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `ZOOM_APP_CLIENT_ID` | Marketplace App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | Marketplace App Credentials | +| `ZOOM_APP_REDIRECT_URI` | Your server URL + /auth | +| `SESSION_SECRET` | Random string for cookie signing | + +## Detailed Guides + +- **[zoom-apps-sdk SKILL.md](../../zoom-apps-sdk/SKILL.md)** - Comprehensive SDK guide +- **[Quick Start](../../zoom-apps-sdk/examples/quick-start.md)** - Hello World app +- **[In-Client OAuth](../../zoom-apps-sdk/examples/in-client-oauth.md)** - Authorization flow +- **[Layers API](../../zoom-apps-sdk/references/layers-api.md)** - Immersive experiences +- **[Immersive Experiences](immersive-experiences.md)** - Custom video layouts +- **[Collaborative Apps](collaborative-apps.md)** - Real-time shared state + +## Skill Chain + +``` +zoom-apps-sdk --> oauth --> zoom-rest-api (optional) +``` + +## App Publishing Checklist + +- [ ] OWASP security headers on all responses +- [ ] HTTPS enforced, valid SSL certificate +- [ ] PKCE OAuth implemented +- [ ] Error handling for all SDK calls +- [ ] Browser preview fallback UI +- [ ] Domain allowlist configured +- [ ] Tested on multiple screen sizes +- [ ] Submit to Zoom Marketplace + +## Resources + +- **Zoom Apps docs**: https://developers.zoom.us/docs/zoom-apps/ +- **Layers API**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Sample apps**: https://github.com/zoom/zoomapps-sample-js diff --git a/partner-built/zoom-plugin/skills/general/use-cases/marketplace-publishing.md b/partner-built/zoom-plugin/skills/general/use-cases/marketplace-publishing.md new file mode 100644 index 00000000..6de0231e --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/marketplace-publishing.md @@ -0,0 +1,384 @@ +# Marketplace Publishing & ISV Guide + +Build and publish apps on the Zoom App Marketplace for multiple customers. + +## Overview + +This guide covers building multi-tenant applications for the Zoom Marketplace, handling multiple customer accounts, and the app review process. + +## Skills Needed + +- **general** - App configuration +- **zoom-rest-api** - Multi-tenant API calls +- **webhooks** - Per-customer event handling + +--- + +## App Types for Marketplace + +| App Type | Visibility | Use Case | +|----------|-----------|----------| +| **Account-level (Private)** | Your org only | Internal tools | +| **User-managed (Public)** | Individual users | User-facing apps | +| **Admin-managed (Public)** | Org admins install | Enterprise tools | + +For Marketplace publishing, you'll create **Public** apps. + +--- + +## Multi-Tenant Architecture + +### Database Schema + +Store per-customer OAuth tokens and settings: + +```sql +CREATE TABLE zoom_installations ( + id SERIAL PRIMARY KEY, + account_id VARCHAR(255) UNIQUE NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + token_expires_at TIMESTAMP NOT NULL, + installed_at TIMESTAMP DEFAULT NOW(), + settings JSONB DEFAULT '{}' +); + +CREATE INDEX idx_zoom_account ON zoom_installations(account_id); +``` + +### OAuth Token Storage + +```javascript +// Store tokens after OAuth callback +async function handleOAuthCallback(code, state) { + // Exchange code for tokens + const tokens = await exchangeCodeForTokens(code); + + // Get account info + const accountInfo = await getAccountInfo(tokens.access_token); + + // Store or update installation + await db.query(` + INSERT INTO zoom_installations + (account_id, access_token, refresh_token, token_expires_at) + VALUES ($1, $2, $3, $4) + ON CONFLICT (account_id) + DO UPDATE SET + access_token = $2, + refresh_token = $3, + token_expires_at = $4 + `, [ + accountInfo.account_id, + tokens.access_token, + tokens.refresh_token, + new Date(Date.now() + tokens.expires_in * 1000) + ]); + + return accountInfo.account_id; +} +``` + +### Token Refresh + +```javascript +async function getValidToken(accountId) { + const installation = await db.query( + 'SELECT * FROM zoom_installations WHERE account_id = $1', + [accountId] + ); + + if (!installation) { + throw new Error('Account not installed'); + } + + // Check if token needs refresh + if (new Date(installation.token_expires_at) < new Date()) { + const newTokens = await refreshTokens(installation.refresh_token); + + await db.query(` + UPDATE zoom_installations + SET access_token = $1, refresh_token = $2, token_expires_at = $3 + WHERE account_id = $4 + `, [ + newTokens.access_token, + newTokens.refresh_token, + new Date(Date.now() + newTokens.expires_in * 1000), + accountId + ]); + + return newTokens.access_token; + } + + return installation.access_token; +} +``` + +--- + +## Webhook Handling for Multi-Tenant + +### Routing by Account + +```javascript +app.post('/webhook', async (req, res) => { + // Verify signature first + if (!verifyWebhookSignature(req)) { + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const accountId = payload.account_id; + + // Check if this account has installed our app + const installation = await getInstallation(accountId); + if (!installation) { + console.log(`Webhook for unknown account: ${accountId}`); + return res.status(200).send(); // Still return 200 + } + + // Process event for this customer + await processEventForCustomer(accountId, event, payload); + + res.status(200).send(); +}); + +async function processEventForCustomer(accountId, event, payload) { + switch (event) { + case 'meeting.started': + await handleMeetingStarted(accountId, payload); + break; + case 'recording.completed': + await handleRecordingCompleted(accountId, payload); + break; + } +} +``` + +### Deauthorization Handling + +When a customer uninstalls your app: + +```javascript +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'app_deauthorized') { + const accountId = payload.account_id; + + // Clean up customer data + await db.query('DELETE FROM zoom_installations WHERE account_id = $1', [accountId]); + + // Optional: Delete customer data per compliance requirements + await cleanupCustomerData(accountId); + + console.log(`App deauthorized for account: ${accountId}`); + } + + res.status(200).send(); +}); +``` + +--- + +## API Calls for Specific Customers + +```javascript +class ZoomAPIClient { + constructor(accountId) { + this.accountId = accountId; + } + + async request(method, endpoint, data = null) { + const token = await getValidToken(this.accountId); + + const response = await axios({ + method, + url: `https://api.zoom.us/v2${endpoint}`, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + data + }); + + return response.data; + } + + async createMeeting(userId, meetingData) { + return this.request('POST', `/users/${userId}/meetings`, meetingData); + } + + async getUsers() { + return this.request('GET', '/users'); + } +} + +// Usage +const client = new ZoomAPIClient('customer_account_id'); +const meeting = await client.createMeeting('me', { topic: 'Team Sync' }); +``` + +--- + +## Scopes for Marketplace Apps + +Request minimal scopes needed: + +```javascript +// Good - specific scopes +const scopes = [ + 'meeting:read', + 'meeting:write', + 'user:read' +]; + +// Bad - overly broad +const scopes = [ + 'account:read:admin', + 'account:write:admin' +]; +``` + +### Scope Descriptions + +Provide clear descriptions for each scope in Marketplace: + +| Scope | User-Facing Description | +|-------|------------------------| +| `meeting:read` | View your meetings | +| `meeting:write` | Create and update meetings on your behalf | +| `recording:read` | Access your meeting recordings | + +--- + +## App Review Process + +### Pre-Submission Checklist + +- [ ] All required scopes have descriptions +- [ ] Privacy policy URL is valid and accessible +- [ ] Terms of service URL is valid +- [ ] Support email/URL is configured +- [ ] App description is clear and accurate +- [ ] Screenshots show actual app functionality +- [ ] Deauthorization webhook handles cleanup +- [ ] OAuth flow completes successfully +- [ ] Error handling is user-friendly + +### Common Rejection Reasons + +1. **Excessive scopes** - Only request what you need +2. **Missing deauthorization handling** - Must handle `app_deauthorized` +3. **Broken OAuth flow** - Test thoroughly +4. **Poor error messages** - Be user-friendly +5. **Privacy policy issues** - Must cover Zoom data usage +6. **Non-functional features** - All advertised features must work + +### Testing Before Submission + +```javascript +// Test OAuth flow +async function testOAuthFlow() { + // 1. Generate auth URL + const authUrl = generateAuthUrl(); + console.log('Auth URL:', authUrl); + + // 2. Complete OAuth manually in browser + // 3. Verify token storage + + // 4. Test API calls + const client = new ZoomAPIClient(testAccountId); + const users = await client.getUsers(); + console.log('Users:', users); + + // 5. Test webhook handling + await simulateWebhook('meeting.started', testPayload); +} + +// Test deauthorization +async function testDeauthorization() { + // Simulate deauth webhook + await simulateWebhook('app_deauthorized', { + account_id: testAccountId + }); + + // Verify cleanup + const installation = await getInstallation(testAccountId); + console.assert(installation === null, 'Installation should be deleted'); +} +``` + +--- + +## Data Residency & Compliance + +### Handle Regional Requirements + +```javascript +// Determine storage region based on user location +async function getStorageRegion(accountId) { + const accountInfo = await zoomClient.getAccountInfo(accountId); + + // Map Zoom data center to storage region + const regionMap = { + 'US': 'us-east-1', + 'EU': 'eu-west-1', + 'AU': 'ap-southeast-2', + 'IN': 'ap-south-1' + }; + + return regionMap[accountInfo.data_residency_region] || 'us-east-1'; +} + +// Store data in correct region +async function storeData(accountId, data) { + const region = await getStorageRegion(accountId); + const regionalStorage = getStorageClient(region); + + await regionalStorage.put(data); +} +``` + +--- + +## Rate Limiting for Multi-Tenant + +Implement per-customer rate limiting: + +```javascript +const rateLimit = require('express-rate-limit'); +const RedisStore = require('rate-limit-redis'); + +const apiLimiter = rateLimit({ + store: new RedisStore({ + client: redisClient, + prefix: 'rl:' + }), + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute per customer + keyGenerator: (req) => { + // Rate limit per customer account + return req.headers['x-account-id'] || req.ip; + } +}); + +app.use('/api/', apiLimiter); +``` + +--- + +## Publishing Steps + +1. **Development** - Build and test thoroughly +2. **Submit for Review** - In Marketplace portal +3. **Review Period** - 2-4 weeks typically +4. **Address Feedback** - Fix any issues found +5. **Approval** - App goes live +6. **Maintenance** - Monitor, update, support + +## Resources + +- **Marketplace Portal**: https://marketplace.zoom.us/ +- **Publishing Guide**: https://developers.zoom.us/docs/zoom-apps/publishing/ +- **App Review**: https://developers.zoom.us/docs/distribute/app-review/ +- **ISV Program**: https://zoom.us/partners/isv diff --git a/partner-built/zoom-plugin/skills/general/use-cases/meeting-automation.md b/partner-built/zoom-plugin/skills/general/use-cases/meeting-automation.md new file mode 100644 index 00000000..77ae84c7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/meeting-automation.md @@ -0,0 +1,239 @@ +# Meeting Automation + +Schedule, update, and delete Zoom meetings programmatically. + +## Overview + +Use the Zoom REST API to automate meeting management - create meetings, update settings, manage participants, and delete meetings without manual intervention. + +If your primary goal is deterministic backend automation, stay on REST API. +If your primary goal is AI-agent tool invocation (for example, natural-language meeting management), route to `zoom-mcp` instead. + +## Skills Needed + +- **zoom-rest-api** - Primary +- **zoom-mcp** - Optional alternative for AI-driven tool workflows + +## Prerequisites + +- Server-to-Server OAuth or OAuth app +- `meeting:write` scope + +## Quick Start + +```bash +# Create a meeting +curl -X POST "https://api.zoom.us/v2/users/me/meetings" \ + -H "Authorization: Bearer {accessToken}" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Automated Meeting", + "type": 2, + "start_time": "2024-01-15T10:00:00Z", + "duration": 60 + }' +``` + +## Common Tasks + +### Creating Recurring Meetings + +```javascript +const axios = require('axios'); + +// Daily recurring meeting +async function createDailyMeeting() { + const response = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: 'Daily Standup', + type: 8, // Recurring with fixed time + start_time: '2024-01-15T09:00:00Z', + duration: 15, + timezone: 'America/Los_Angeles', + recurrence: { + type: 1, // Daily + repeat_interval: 1, // Every day + end_times: 90 // 90 occurrences + }, + settings: { + join_before_host: true, + waiting_room: false, + mute_upon_entry: true + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return response.data; +} + +// Weekly recurring meeting +async function createWeeklyMeeting() { + const response = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: 'Weekly Team Sync', + type: 8, + start_time: '2024-01-15T14:00:00Z', + duration: 60, + recurrence: { + type: 2, // Weekly + repeat_interval: 1, + weekly_days: '2,4', // Monday, Wednesday (1=Sun, 2=Mon, etc.) + end_date_time: '2024-12-31T00:00:00Z' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + return response.data; +} +``` + +### Updating Meeting Settings + +```javascript +// Update meeting details +async function updateMeeting(meetingId, updates) { + await axios.patch( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + topic: updates.topic, + start_time: updates.startTime, + duration: updates.duration, + settings: { + host_video: updates.hostVideo ?? true, + participant_video: updates.participantVideo ?? false, + join_before_host: updates.joinBeforeHost ?? false, + waiting_room: updates.waitingRoom ?? true, + mute_upon_entry: updates.muteOnEntry ?? true, + auto_recording: updates.autoRecording ?? 'none' // 'local', 'cloud', 'none' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// Add meeting co-hosts +async function addCoHosts(meetingId, emails) { + // Co-hosts must be set before meeting starts + await axios.patch( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + settings: { + alternative_hosts: emails.join(';'), + alternative_hosts_email_notification: true + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Managing Registrants + +```javascript +// Add registrant +async function addRegistrant(meetingId, registrant) { + const response = await axios.post( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants`, + { + email: registrant.email, + first_name: registrant.firstName, + last_name: registrant.lastName, + custom_questions: [ + { title: 'Company', value: registrant.company }, + { title: 'Role', value: registrant.role } + ] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + // Returns join_url for the registrant + return response.data; +} + +// List registrants +async function listRegistrants(meetingId, status = 'approved') { + // status: 'pending', 'approved', 'denied' + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants?status=${status}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + return response.data.registrants; +} + +// Approve/deny registrants +async function updateRegistrantStatus(meetingId, registrantId, action) { + // action: 'approve', 'deny', 'cancel' + await axios.put( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants/status`, + { + action: action, + registrants: [{ id: registrantId }] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Deleting/Canceling Meetings + +```javascript +// Delete meeting +async function deleteMeeting(meetingId, notifyHosts = true) { + await axios.delete( + `https://api.zoom.us/v2/meetings/${meetingId}?schedule_for_reminder=${notifyHosts}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// Delete specific occurrence of recurring meeting +async function deleteOccurrence(meetingId, occurrenceId) { + await axios.delete( + `https://api.zoom.us/v2/meetings/${meetingId}?occurrence_id=${occurrenceId}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// End a live meeting +async function endMeeting(meetingId) { + await axios.put( + `https://api.zoom.us/v2/meetings/${meetingId}/status`, + { action: 'end' }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Rate Limit Considerations + +| Operation | Limit | +|-----------|-------| +| Create/Update meetings | 100 per user per day | +| API calls (Light) | 30/sec (Pro), 80/sec (Business+) | +| API calls (Heavy) | 10/sec (Pro), 40/sec (Business+) | + +```javascript +// Implement rate limiting +const Bottleneck = require('bottleneck'); + +const limiter = new Bottleneck({ + minTime: 100, // 10 requests per second max + maxConcurrent: 5 +}); + +const createMeetingLimited = limiter.wrap(createMeeting); +``` + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Rate Limits**: https://developers.zoom.us/docs/api/rest/rate-limits/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/meeting-bots.md b/partner-built/zoom-plugin/skills/general/use-cases/meeting-bots.md new file mode 100644 index 00000000..de348ef6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/meeting-bots.md @@ -0,0 +1,311 @@ +# Meeting Bots + +Build bots that join Zoom meetings for AI, transcription, and automation. + +## Overview + +Meeting bots are headless applications that join Zoom meetings as participants to perform tasks like recording, transcription, real-time AI processing, or automated interactions. + +## Skills Needed + +- **meeting-sdk/linux** - Visible bot join flow, raw recording, and raw media access +- **zoom-rest-api** - Meeting lookup plus OBF/ZAK retrieval, optional cloud-recording settings +- **zoom-webhooks** - Optional if you want Zoom-managed cloud recording download after the meeting +- **zoom-rtms** - Alternative when you need invisible media access instead of a visible participant bot + +## Architecture + +``` +Meeting Bot Architecture: +┌─────────────────┐ ┌─────────────────┐ +│ Zoom Meeting │────▶│ Bot (Linux) │ +│ │ │ - Meeting SDK │ +│ │◀────│ - Raw audio │ +└─────────────────┘ │ - Raw video │ + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ AI Pipeline │ + │ - Transcription│ + │ - Analysis │ + └─────────────────┘ +``` + +## Platform + +| Platform | Recommended | +|----------|-------------| +| Linux | Yes - headless, server-side | +| Windows | Possible but not typical | +| macOS | Possible but not typical | + +## Key Features + +- Join meetings programmatically +- Access raw audio/video data +- Start and stop raw recording explicitly +- Real-time transcription +- AI processing (sentiment, summarization) + +## Automatic Join + Recording Pattern + +Use this chain when the user asks for a bot that automatically joins and records a meeting: + +```text +zoom-rest-api + -> fetch meeting metadata + -> mint OBF/ZAK token +meeting-sdk/linux + -> join as visible participant + -> StartRawRecording() + -> subscribe audio/video delegates + -> write PCM/YUV or send to downstream pipeline +optional zoom-webhooks + zoom-rest-api + -> receive recording.completed + -> download Zoom-managed cloud recording assets +``` + +### Raw Recording Control + +```cpp +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status != MEETING_STATUS_INMEETING) return; + + auto* recordCtrl = m_meetingService->GetMeetingRecordingController(); + if (!recordCtrl) { + throw std::runtime_error("recording_controller_unavailable"); + } + + if (recordCtrl->CanStartRawRecording() != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); + } + + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); + } + + GetAudioRawdataHelper()->subscribe(new AudioRawDataDelegate(), true); + + IZoomSDKRenderer* renderer = nullptr; + createRenderer(&renderer, new VideoRawDataDelegate()); + renderer->setRawDataResolution(ZoomSDKResolution_720P); + renderer->subscribe(activeSpeakerUserId, RAW_DATA_TYPE_VIDEO); +} +``` + +### Choose the Right Recording Output + +| Requirement | Correct path | +|-------------|--------------| +| Bot-owned audio/video files or real-time AI processing | Meeting SDK Linux raw recording | +| Zoom-hosted MP4/M4A/transcript files after meeting end | Cloud recording settings + webhooks + recordings REST API | + +`StartRawRecording()` enables raw media flow. It does not create a finished MP4 by itself. You still need to persist PCM/YUV or post-process it with your own pipeline. + +## Bot Implementation Patterns + +### 1. Bot Authentication Flow + +```cpp +// Step 1: Generate JWT for SDK authentication +void generateJWT(const string& key, const string& secret) { + auto iat = chrono::system_clock::now(); + auto exp = iat + chrono::hours{24}; + + m_jwt = jwt::create() + .set_type("JWT") + .set_issued_at(iat) + .set_expires_at(exp) + .set_payload_claim("appKey", claim(key)) + .set_payload_claim("tokenExp", claim(exp)) + .sign(algorithm::hs256{secret}); +} + +// Step 2: Authenticate SDK +AuthContext ctx; +ctx.jwt_token = m_jwt; +m_authService->SDKAuth(ctx); +// Wait for onAuthenticationReturn callback +``` + +### 2. Joining a Meeting as a Bot + +```cpp +JoinParam joinParam; +joinParam.userType = SDK_UT_WITHOUT_LOGIN; +JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; +param.meetingNumber = meetingNumber; +param.userName = "My Transcription Bot"; // Display name +param.psw = password; +param.isVideoOff = true; // Bots typically don't need video +param.isAudioOff = false; // Need audio for transcription + +// For own meetings: Use ZAK token +param.userZAK = zakToken; + +// For external meetings (after Feb 2026): Use OBF token +param.onBehalfToken = obfToken; + +err = m_meetingService->Join(joinParam); +``` + +**Token Requirements:** + +| Meeting Type | Required Tokens | +|--------------|-----------------| +| Your own meetings | JWT + ZAK | +| External meetings (before Feb 2026) | JWT only | +| External meetings (after Feb 2026) | JWT + OBF | + +### 3. Capturing Audio Streams + +```cpp +class AudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData *data) override { + // Mixed audio from all participants + // Format: 16-bit PCM, 16kHz or 32kHz, mono + + // Send to transcription service + transcriptionService.process( + data->GetBuffer(), + data->GetBufferLen(), + data->GetSampleRate() + ); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Individual participant audio - useful for speaker identification + speakerIdentifier.process(node_id, data); + } +}; + +// Subscribe after joining meeting +auto* pRawDataHelper = GetAudioRawdataHelper(); +pRawDataHelper->subscribe(new AudioRawDataDelegate()); +``` + +### 4. Processing Video Frames + +```cpp +class VideoRawDataDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420 *data) override { + // Format: I420 (YUV 4:2:0) - contiguous planar data + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Option 1: Save raw YUV for later processing + yuvFile.write(data->GetYBuffer(), width * height); + yuvFile.write(data->GetUBuffer(), (width/2) * (height/2)); + yuvFile.write(data->GetVBuffer(), (width/2) * (height/2)); + + // Option 2: Convert to OpenCV for real-time processing + // (requires copying planes into contiguous buffer first) + } +}; + +// Subscribe to specific user's video +auto* pVideoHelper = GetRawdataRendererHelper(); +pVideoHelper->setRawDataResolution(ZoomSDKResolution_720P); +pVideoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +### 5. Handling Participant Events + +```cpp +class MeetingParticipantsDelegate : public IMeetingParticipantsCtrlEvent { +public: + void onUserJoin(IList* lstUserID, const wchar_t* strUserList) override { + // New participants joined + for (int i = 0; i < lstUserID->GetCount(); i++) { + unsigned int userId = lstUserID->GetItem(i); + auto userInfo = m_participantsCtrl->GetUserByUserID(userId); + log("User joined: " + userInfo->GetUserName()); + + // Subscribe to their video if needed + subscribeToUserVideo(userId); + } + } + + void onUserLeft(IList* lstUserID, const wchar_t* strUserList) override { + // Participants left + } + + void onHostChangeNotification(unsigned int userId) override { + // Host changed + } +}; +``` + +### 6. Graceful Disconnection + +```cpp +void Bot::leaveMeeting() { + // Stop raw data subscriptions + GetAudioRawdataHelper()->unSubscribe(); + + // Stop recording if active + auto recCtl = m_meetingService->GetMeetingRecordingController(); + recCtl->StopRawRecording(); + + // Leave meeting + m_meetingService->Leave(LEAVE_MEETING); + + // Wait for onMeetingStatusChanged(MEETING_STATUS_ENDED) +} + +// Handle unexpected disconnection +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_ENDED || status == MEETING_STATUS_FAILED) { + cleanup(); + // Optionally reconnect + if (shouldReconnect) { + scheduleReconnect(); + } + } +} +``` + +## Scaling Considerations + +| Consideration | Recommendation | +|---------------|----------------| +| **Bot per meeting** | 1 bot instance per meeting (SDK limitation) | +| **Container deployment** | Use Kubernetes with 1 pod per bot | +| **Resource allocation** | 2-4 GB RAM, 1-2 CPU cores per bot | +| **Queue management** | Use message queue (Redis, RabbitMQ) for bot assignments | +| **Health monitoring** | Implement heartbeat checks for bot instances | + +## Meeting SDK vs Video SDK for Bots + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| Joins | Zoom meetings | Video SDK sessions | +| Features | Full meeting features | Core video features | +| Use case | Meeting bots, recording | Custom video apps, telehealth | + +**Choose Meeting SDK** for: Joining existing Zoom meetings, transcription bots +**Choose Video SDK** for: Custom video sessions you control, BYOS recording + +## Detailed Platform Guides + +### Meeting SDK (Linux) - Recommended for Bots +- **[Meeting SDK Linux - Quick Start](../../meeting-sdk/linux/linux.md)** - Complete setup guide +- **[High-Level Bot Scenarios](../../meeting-sdk/linux/concepts/high-level-scenarios.md)** - Production architectures +- **[Resilient Bot Pattern](../../meeting-sdk/linux/meeting-sdk-bot.md)** - Retry logic, OBF tokens +- **[Linux Platform Reference](../../meeting-sdk/linux/references/linux-reference.md)** - Dependencies, Docker, troubleshooting + +### Specific Use Cases +- **[Transcription Bot (Linux)](transcription-bot-linux.md)** - Step-by-step transcription bot guide +- **[AI Integration](ai-integration.md)** - AI-powered meeting analysis +- **[Real-time Media Streams](real-time-media-streams.md)** - RTMS alternative (invisible bots) + +## Resources + +- **Meeting SDK Linux Docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Meeting SDK Linux API**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +- **Headless Sample (Modern)**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Raw Recording Sample (Traditional)**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- **RTMS (Alternative)**: https://developers.zoom.us/docs/rtms/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/meeting-details-with-events.md b/partner-built/zoom-plugin/skills/general/use-cases/meeting-details-with-events.md new file mode 100644 index 00000000..3bc292d4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/meeting-details-with-events.md @@ -0,0 +1,630 @@ +# Meeting Details with Event Subscription + +Retrieve meeting details and subscribe to real-time meeting events. + +## Overview + +A common integration pattern: get meeting information via REST API, then receive real-time updates via webhooks when meeting events occur (started, ended, participants join/leave). + +For implementation-heavy orchestration patterns (token refresh locks, retries, queue-based webhook handling, circuit-breaker and reconciliation fallbacks), see: +- [../references/automatic-skill-chaining-rest-webhooks.md](../references/automatic-skill-chaining-rest-webhooks.md) +- [../references/meeting-webhooks-oauth-refresh-orchestration.md](../references/meeting-webhooks-oauth-refresh-orchestration.md) +- [../references/distributed-meeting-fallback-architecture.md](../references/distributed-meeting-fallback-architecture.md) + +## Skills Needed + +| Order | Skill | Purpose | +|-------|-------|---------| +| 1 | **zoom-rest-api** | Retrieve meeting details | +| 2 | **webhooks** | Subscribe to and receive meeting events | + +## Skill Chaining Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ COMPLETE INTEGRATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +SETUP PHASE (One-time): +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Configure Event Subscriptions (Marketplace Portal or API) │ +│ └── Subscribe to: meeting.started, meeting.ended, │ +│ meeting.participant_joined, meeting.participant_left │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +RUNTIME PHASE: +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. GET Meeting Details (zoom-rest-api) │ +│ └── GET /meetings/{meetingId} │ +│ └── Store meeting info (topic, host, settings, join_url) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Receive Meeting Events (webhooks) │ +│ └── meeting.started → Update status, notify users │ +│ └── meeting.participant_joined → Track attendance │ +│ └── meeting.participant_left → Log departure time │ +│ └── meeting.ended → Finalize records, trigger post-processing │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- Zoom app with OAuth or Server-to-Server OAuth +- Scopes: `meeting:read` (for REST API) +- Event subscriptions configured for meeting events +- HTTPS endpoint for receiving webhooks +- **See [Authorization Patterns](../references/authorization-patterns.md)** for scope validation and permission checking + +## Step 1: Configure Event Subscriptions + +### Option A: Via Marketplace Portal (Recommended for most apps) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us/) → Your App +2. Navigate to **Feature** → **Event Subscriptions** +3. Click **Add Event Subscription** +4. Configure: + - **Subscription name**: "Meeting Events" + - **Event notification endpoint URL**: `https://yourapp.com/webhooks/zoom` +5. Select events: + - ✅ `meeting.started` + - ✅ `meeting.ended` + - ✅ `meeting.participant_joined` + - ✅ `meeting.participant_left` +6. Save and activate + +### Option B: Programmatic Setup + +Event subscriptions are configured at app creation time in the Marketplace portal. However, you can verify your subscription status via API: + +```javascript +// List webhook subscriptions for your app +const response = await fetch( + 'https://api.zoom.us/v2/webhooks', + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } +); + +const webhooks = await response.json(); +console.log('Active webhooks:', webhooks); +``` + +## Step 2: Retrieve Meeting Details (zoom-rest-api) + +```javascript +const axios = require('axios'); + +/** + * Get meeting details from Zoom REST API + * @param {string} meetingId - The meeting ID + * @param {string} accessToken - Valid OAuth access token + * @returns {Promise} Meeting details + */ +async function getMeetingDetails(meetingId, accessToken) { + try { + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + } + ); + + const meeting = response.data; + + return { + id: meeting.id, + uuid: meeting.uuid, + topic: meeting.topic, + type: meeting.type, + status: meeting.status, + start_time: meeting.start_time, + duration: meeting.duration, + timezone: meeting.timezone, + host_id: meeting.host_id, + host_email: meeting.host_email, + join_url: meeting.join_url, + password: meeting.password, + settings: meeting.settings + }; + } catch (error) { + if (error.response?.status === 404) { + throw new Error(`Meeting ${meetingId} not found`); + } + if (error.response?.status === 401) { + throw new Error('Invalid or expired access token'); + } + throw error; + } +} + +// Usage +const meeting = await getMeetingDetails('123456789', accessToken); +console.log(`Meeting: ${meeting.topic}`); +console.log(`Join URL: ${meeting.join_url}`); +console.log(`Host: ${meeting.host_email}`); +``` + +## Step 3: Handle Meeting Events (webhooks) + +```javascript +const express = require('express'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// Store meeting state (use database in production) +const meetingState = new Map(); + +/** + * Verify Zoom webhook signature + */ +function verifyWebhookSignature(req, webhookSecret) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const expectedSignature = `v0=${crypto + .createHmac('sha256', webhookSecret) + .update(payload) + .digest('hex')}`; + + return signature === expectedSignature; +} + +/** + * Handle URL validation challenge (required for new subscriptions) + */ +function handleUrlValidation(req, res, webhookSecret) { + const hashForValidation = crypto + .createHmac('sha256', webhookSecret) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hashForValidation + }); +} + +// Webhook endpoint +app.post('/webhooks/zoom', async (req, res) => { + const WEBHOOK_SECRET = process.env.ZOOM_WEBHOOK_SECRET; + + // 1. Handle URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + return handleUrlValidation(req, res, WEBHOOK_SECRET); + } + + // 2. Verify webhook signature + if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) { + console.error('Invalid webhook signature'); + return res.status(401).send('Invalid signature'); + } + + // 3. Process meeting events + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + console.log(`Received event: ${event} for meeting ${meetingId}`); + + try { + switch (event) { + case 'meeting.started': + await handleMeetingStarted(meetingId, payload); + break; + case 'meeting.ended': + await handleMeetingEnded(meetingId, payload); + break; + case 'meeting.participant_joined': + await handleParticipantJoined(meetingId, payload); + break; + case 'meeting.participant_left': + await handleParticipantLeft(meetingId, payload); + break; + default: + console.log(`Unhandled event: ${event}`); + } + + res.status(200).send(); + } catch (error) { + console.error(`Error handling ${event}:`, error); + // Return 200 to prevent Zoom from retrying + // Log error for investigation + res.status(200).send(); + } +}); + +// Event handlers +async function handleMeetingStarted(meetingId, payload) { + const { object } = payload; + + // Initialize meeting state + meetingState.set(meetingId, { + topic: object.topic, + host_id: object.host_id, + start_time: object.start_time, + participants: [], + status: 'in_progress' + }); + + console.log(`✅ Meeting started: ${object.topic} (ID: ${meetingId})`); + + // Optional: Fetch full meeting details for additional context + // const fullDetails = await getMeetingDetails(meetingId, accessToken); +} + +async function handleMeetingEnded(meetingId, payload) { + const { object } = payload; + const state = meetingState.get(meetingId); + + if (state) { + state.status = 'ended'; + state.end_time = object.end_time; + state.duration = object.duration; + + console.log(`🏁 Meeting ended: ${state.topic}`); + console.log(` Duration: ${state.duration} minutes`); + console.log(` Total participants: ${state.participants.length}`); + + // Trigger post-meeting processing + await processMeetingRecords(meetingId, state); + } +} + +async function handleParticipantJoined(meetingId, payload) { + const { object } = payload; + const participant = object.participant; + const state = meetingState.get(meetingId); + + if (state) { + state.participants.push({ + user_id: participant.user_id, + user_name: participant.user_name, + email: participant.email, + join_time: participant.join_time, + status: 'in_meeting' + }); + + console.log(`👋 ${participant.user_name} joined meeting ${meetingId}`); + } +} + +async function handleParticipantLeft(meetingId, payload) { + const { object } = payload; + const participant = object.participant; + const state = meetingState.get(meetingId); + + if (state) { + const p = state.participants.find(p => p.user_id === participant.user_id); + if (p) { + p.leave_time = participant.leave_time; + p.status = 'left'; + } + + console.log(`👋 ${participant.user_name} left meeting ${meetingId}`); + } +} + +async function processMeetingRecords(meetingId, state) { + // Save to database, generate reports, notify stakeholders, etc. + console.log('Processing meeting records...'); + + // Example: Calculate attendance stats + const attendanceReport = { + meeting_id: meetingId, + topic: state.topic, + duration_minutes: state.duration, + total_participants: state.participants.length, + participants: state.participants.map(p => ({ + name: p.user_name, + email: p.email, + joined: p.join_time, + left: p.leave_time || state.end_time + })) + }; + + console.log(JSON.stringify(attendanceReport, null, 2)); + + // Clean up in-memory state + meetingState.delete(meetingId); +} + +app.listen(3000, () => { + console.log('Webhook server running on port 3000'); +}); +``` + +## Complete Integration Example + +```javascript +/** + * Complete example: Meeting Dashboard Integration + * + * Skills used: + * 1. zoom-rest-api - Get meeting details + * 2. webhooks - Real-time event updates + */ + +const express = require('express'); +const axios = require('axios'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// In-memory store (use Redis/database in production) +const meetings = new Map(); + +// ============================================ +// AUTHENTICATION HELPER +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +// ============================================ +// STEP 1: REST API - Get Meeting Details +// ============================================ + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// API endpoint to fetch and track a meeting +app.get('/api/meetings/:meetingId', async (req, res) => { + try { + const { meetingId } = req.params; + + // Get meeting details from Zoom API (zoom-rest-api skill) + const details = await getMeetingDetails(meetingId); + + // Store for tracking (webhook events will update this) + meetings.set(meetingId, { + ...details, + participants: [], + events: [], + tracking_status: 'active' + }); + + res.json({ + success: true, + meeting: { + id: details.id, + topic: details.topic, + start_time: details.start_time, + join_url: details.join_url, + host_email: details.host_email + }, + message: 'Meeting tracked. Events will be received via webhook.' + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + success: false, + error: error.message + }); + } +}); + +// ============================================ +// STEP 2: WEBHOOKS - Receive Meeting Events +// ============================================ + +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + const expected = `v0=${crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(payload) + .digest('hex')}`; + + return signature === expected; +} + +app.post('/webhooks/zoom', async (req, res) => { + // URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hash + }); + } + + // Verify signature + if (!verifyWebhook(req)) { + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + // Get or create meeting record + let meeting = meetings.get(meetingId); + if (!meeting) { + // Meeting wasn't pre-fetched, create minimal record + meeting = { + id: meetingId, + topic: payload.object.topic, + participants: [], + events: [], + tracking_status: 'webhook_only' + }; + meetings.set(meetingId, meeting); + } + + // Log event + meeting.events.push({ + type: event, + timestamp: new Date().toISOString(), + data: payload + }); + + // Update meeting state based on event + switch (event) { + case 'meeting.started': + meeting.status = 'in_progress'; + meeting.actual_start_time = payload.object.start_time; + console.log(`✅ Meeting started: ${meeting.topic}`); + break; + + case 'meeting.ended': + meeting.status = 'ended'; + meeting.end_time = payload.object.end_time; + meeting.actual_duration = payload.object.duration; + console.log(`🏁 Meeting ended: ${meeting.topic} (${meeting.actual_duration} min)`); + break; + + case 'meeting.participant_joined': + meeting.participants.push({ + ...payload.object.participant, + status: 'in_meeting' + }); + console.log(`👋 ${payload.object.participant.user_name} joined`); + break; + + case 'meeting.participant_left': + const p = meeting.participants.find( + p => p.user_id === payload.object.participant.user_id + ); + if (p) { + p.status = 'left'; + p.leave_time = payload.object.participant.leave_time; + } + console.log(`👋 ${payload.object.participant.user_name} left`); + break; + } + + res.status(200).send(); +}); + +// ============================================ +// STEP 3: Query Meeting Status +// ============================================ + +app.get('/api/meetings/:meetingId/status', (req, res) => { + const meeting = meetings.get(req.params.meetingId); + + if (!meeting) { + return res.status(404).json({ error: 'Meeting not tracked' }); + } + + res.json({ + id: meeting.id, + topic: meeting.topic, + status: meeting.status || 'scheduled', + participants_in_meeting: meeting.participants.filter(p => p.status === 'in_meeting').length, + total_participants: meeting.participants.length, + events_count: meeting.events.length, + last_event: meeting.events[meeting.events.length - 1]?.type + }); +}); + +// ============================================ +// START SERVER +// ============================================ + +app.listen(3000, () => { + console.log('Server running on port 3000'); + console.log(''); + console.log('Endpoints:'); + console.log(' GET /api/meetings/:id - Fetch & track meeting (zoom-rest-api)'); + console.log(' GET /api/meetings/:id/status - Get meeting status'); + console.log(' POST /webhooks/zoom - Webhook receiver (webhooks)'); +}); +``` + +## Meeting Event Types Reference + +| Event | Trigger | Key Payload Fields | +|-------|---------|-------------------| +| `meeting.started` | Host starts meeting | `id`, `topic`, `host_id`, `start_time` | +| `meeting.ended` | Meeting ends | `id`, `end_time`, `duration` | +| `meeting.participant_joined` | User joins | `participant.user_id`, `participant.user_name`, `participant.join_time` | +| `meeting.participant_left` | User leaves | `participant.user_id`, `participant.leave_time` | +| `meeting.sharing_started` | Screen share begins | `participant`, `sharing_details` | +| `meeting.sharing_ended` | Screen share ends | `participant` | + +## Error Handling + +### Common Errors and Solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| 404 on GET /meetings | Invalid meeting ID | Verify meeting ID exists | +| 401 on API call | Expired token | Refresh access token | +| Invalid webhook signature | Wrong secret or modified payload | Verify WEBHOOK_SECRET matches app config | +| Missing events | Subscription not active | Check Event Subscriptions in Marketplace | + +### Retry Logic for REST API + +```javascript +async function getMeetingWithRetry(meetingId, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await getMeetingDetails(meetingId); + } catch (error) { + if (error.response?.status === 429) { + // Rate limited - wait and retry + const retryAfter = error.response.headers['retry-after'] || 1; + await new Promise(r => setTimeout(r, retryAfter * 1000)); + continue; + } + if (error.response?.status === 401 && attempt < maxRetries) { + // Token expired - refresh and retry + await refreshAccessToken(); + continue; + } + throw error; + } + } +} +``` + +## Best Practices + +1. **Fetch meeting details first** - Get context before events arrive +2. **Handle events idempotently** - Webhooks may be delivered multiple times +3. **Use meeting UUID for tracking** - More reliable than meeting ID for recurring meetings +4. **Store events for audit** - Log all events for debugging and compliance +5. **Implement retry logic** - REST API calls may fail transiently +6. **Return 200 for webhooks** - Even on processing errors, to prevent retries + +## Related Use Cases + +- **[Recording & Transcription](recording-transcription.md)** - Download recordings after meeting ends +- **[Meeting Automation](meeting-automation.md)** - Create and manage meetings programmatically +- **[Real-Time Media Streams](real-time-media-streams.md)** - Access live audio/video during meeting + +## Resources + +- **Meetings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Webhook Events**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Event Subscriptions**: https://developers.zoom.us/docs/api/rest/webhook-reference/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/meeting-links-vs-embedding.md b/partner-built/zoom-plugin/skills/general/use-cases/meeting-links-vs-embedding.md new file mode 100644 index 00000000..49b16c61 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/meeting-links-vs-embedding.md @@ -0,0 +1,38 @@ +# Meeting Links vs Embedding (REST `join_url` vs Meeting SDK) + +This is a high-frequency confusion cluster: + +- "Generate Zoom Meeting URLs server-side" +- "Join meeting via API" +- "Can I use the `join_url` inside Meeting SDK?" + +## The Decision + +### Use `join_url` (Meeting Link) When + +- You are sending a user to the Zoom client or Zoom web client. +- You do not need embedded UI inside your application. + +### Use Meeting SDK When + +- You must embed a meeting inside your app. +- You need SDK-level control over the experience. + +## Common Mistakes + +- Treating REST `join_url` as a way to join via SDK. +- Expecting an API endpoint to "join a user to a meeting". + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-rest-api** | Create meetings and understand what `join_url` is | +| 2 | **zoom-meeting-sdk** | Embed meetings correctly | + +## Links + +- `../../rest-api/concepts/meeting-urls-and-sdk-joining.md` +- `../../meeting-sdk/SKILL.md` +- `embed-meetings.md` + diff --git a/partner-built/zoom-plugin/skills/general/use-cases/minutes-calculation.md b/partner-built/zoom-plugin/skills/general/use-cases/minutes-calculation.md new file mode 100644 index 00000000..7d262a11 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/minutes-calculation.md @@ -0,0 +1,798 @@ +# Minutes Calculation for Billing + +Calculate usage minutes for Video SDK sessions and Meeting SDK meetings for billing and cost management. + +## Overview + +Zoom SDKs are billed based on **participant-minutes**. This guide covers how to track and calculate usage for accurate billing projections and cost optimization. + +## Skills Needed + +- **zoom-rest-api** - Reports API +- **webhooks** - Real-time tracking +- **zoom-video-sdk** - Session Quality API + +## Billing Models + +| SDK | Billing Unit | Calculation | +|-----|--------------|-------------| +| Video SDK | Participant-minutes | Sum of (each participant's session duration) | +| Meeting SDK | Host minutes | Based on meeting duration, not participant count | + +**Example**: A 30-minute Video SDK session with 4 participants = 120 participant-minutes. + +## Video SDK Usage Tracking + +### Method 1: Session Quality API (Recommended) + +Most accurate method using Zoom's built-in analytics. + +```javascript +const axios = require('axios'); + +// Get session details including participant minutes +async function getSessionUsage(sessionId) { + const response = await axios.get( + `https://api.zoom.us/v2/videosdk/sessions/${sessionId}`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return { + sessionName: response.data.session_name, + startTime: response.data.start_time, + endTime: response.data.end_time, + totalMinutes: response.data.duration, // Total session minutes + participantCount: response.data.participant_count + }; +} + +// Get participant-level breakdown +async function getSessionParticipants(sessionId) { + const response = await axios.get( + `https://api.zoom.us/v2/videosdk/sessions/${sessionId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Calculate participant-minutes + const participants = response.data.participants.map(p => { + const joinTime = new Date(p.join_time); + const leaveTime = new Date(p.leave_time); + const durationMinutes = (leaveTime - joinTime) / 1000 / 60; + + return { + name: p.user_name, + joinTime: p.join_time, + leaveTime: p.leave_time, + durationMinutes: Math.round(durationMinutes * 100) / 100 + }; + }); + + const totalParticipantMinutes = participants.reduce( + (sum, p) => sum + p.durationMinutes, 0 + ); + + return { + participants, + totalParticipantMinutes: Math.round(totalParticipantMinutes * 100) / 100 + }; +} + +// Get all sessions in date range +async function getSessionsInRange(from, to) { + const response = await axios.get( + 'https://api.zoom.us/v2/videosdk/sessions', + { + params: { from, to, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.sessions; +} + +// Calculate monthly usage +async function calculateMonthlyUsage(year, month) { + const from = `${year}-${String(month).padStart(2, '0')}-01`; + const lastDay = new Date(year, month, 0).getDate(); + const to = `${year}-${String(month).padStart(2, '0')}-${lastDay}`; + + const sessions = await getSessionsInRange(from, to); + + let totalParticipantMinutes = 0; + const sessionDetails = []; + + for (const session of sessions) { + const participants = await getSessionParticipants(session.id); + totalParticipantMinutes += participants.totalParticipantMinutes; + + sessionDetails.push({ + sessionId: session.id, + sessionName: session.session_name, + date: session.start_time, + participantMinutes: participants.totalParticipantMinutes + }); + } + + return { + period: `${year}-${String(month).padStart(2, '0')}`, + totalSessions: sessions.length, + totalParticipantMinutes: Math.round(totalParticipantMinutes), + sessions: sessionDetails + }; +} +``` + +### Method 2: Real-Time Webhook Tracking + +Track usage in real-time as sessions happen. + +```javascript +const express = require('express'); +const app = express(); + +// In-memory storage (use database in production) +const activeSessions = new Map(); +const usageRecords = []; + +app.post('/webhook', express.json(), (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'session.started': + handleSessionStarted(payload); + break; + case 'session.ended': + handleSessionEnded(payload); + break; + case 'session.participant_joined': + handleParticipantJoined(payload); + break; + case 'session.participant_left': + handleParticipantLeft(payload); + break; + } + + res.status(200).send(); +}); + +function handleSessionStarted(payload) { + const { object } = payload; + activeSessions.set(object.id, { + sessionId: object.id, + sessionName: object.session_name, + startTime: new Date(object.start_time), + participants: new Map() + }); +} + +function handleSessionEnded(payload) { + const { object } = payload; + const session = activeSessions.get(object.id); + + if (session) { + // Calculate final usage for any remaining participants + const endTime = new Date(object.end_time); + let totalMinutes = 0; + + session.participants.forEach((participant, participantId) => { + if (!participant.leaveTime) { + participant.leaveTime = endTime; + } + const duration = (participant.leaveTime - participant.joinTime) / 1000 / 60; + totalMinutes += duration; + }); + + // Record usage + usageRecords.push({ + sessionId: object.id, + sessionName: session.sessionName, + startTime: session.startTime, + endTime: endTime, + totalParticipantMinutes: Math.round(totalMinutes * 100) / 100, + participantCount: session.participants.size + }); + + activeSessions.delete(object.id); + } +} + +function handleParticipantJoined(payload) { + const { object } = payload; + const session = activeSessions.get(object.session_id); + + if (session) { + session.participants.set(object.participant.participant_id, { + name: object.participant.user_name, + joinTime: new Date(object.participant.join_time), + leaveTime: null + }); + } +} + +function handleParticipantLeft(payload) { + const { object } = payload; + const session = activeSessions.get(object.session_id); + + if (session) { + const participant = session.participants.get(object.participant.participant_id); + if (participant) { + participant.leaveTime = new Date(object.participant.leave_time); + } + } +} + +// Get current month usage +app.get('/usage/current-month', (req, res) => { + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const monthlyRecords = usageRecords.filter(r => + new Date(r.startTime) >= startOfMonth + ); + + const totalMinutes = monthlyRecords.reduce( + (sum, r) => sum + r.totalParticipantMinutes, 0 + ); + + res.json({ + period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, + totalSessions: monthlyRecords.length, + totalParticipantMinutes: Math.round(totalMinutes), + records: monthlyRecords + }); +}); +``` + +### Method 3: Client-Side Tracking + +Track locally in your SDK application. + +```javascript +// React Native / JavaScript SDK tracking +class UsageTracker { + constructor() { + this.sessionStart = null; + this.participants = new Map(); + } + + onSessionJoin() { + this.sessionStart = new Date(); + } + + onUserJoin(user) { + this.participants.set(user.id, { + name: user.name, + joinTime: new Date(), + leaveTime: null + }); + } + + onUserLeave(user) { + const participant = this.participants.get(user.id); + if (participant) { + participant.leaveTime = new Date(); + } + } + + calculateUsage() { + const now = new Date(); + let totalMinutes = 0; + + this.participants.forEach(participant => { + const endTime = participant.leaveTime || now; + const duration = (endTime - participant.joinTime) / 1000 / 60; + totalMinutes += duration; + }); + + return { + sessionDuration: (now - this.sessionStart) / 1000 / 60, + totalParticipantMinutes: Math.round(totalMinutes * 100) / 100, + participantCount: this.participants.size + }; + } + + // Send to your backend periodically + async reportUsage() { + const usage = this.calculateUsage(); + await fetch('/api/usage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(usage) + }); + } +} +``` + +```cpp +// Windows/C++ SDK tracking +class UsageTracker : public IZoomVideoSDKDelegate { +private: + std::chrono::system_clock::time_point sessionStart; + struct ParticipantUsage { + std::wstring name; + std::chrono::system_clock::time_point joinTime; + std::chrono::system_clock::time_point leaveTime; + bool hasLeft = false; + }; + std::map participants; + +public: + void onSessionJoin() override { + sessionStart = std::chrono::system_clock::now(); + } + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* users) override { + for (int i = 0; i < users->GetCount(); i++) { + auto user = users->GetItem(i); + ParticipantUsage usage; + usage.name = user->getUserName(); + usage.joinTime = std::chrono::system_clock::now(); + participants[user->getUserID()] = usage; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* users) override { + for (int i = 0; i < users->GetCount(); i++) { + auto user = users->GetItem(i); + auto it = participants.find(user->getUserID()); + if (it != participants.end()) { + it->second.leaveTime = std::chrono::system_clock::now(); + it->second.hasLeft = true; + } + } + } + + double calculateTotalMinutes() { + auto now = std::chrono::system_clock::now(); + double totalMinutes = 0; + + for (const auto& [id, participant] : participants) { + auto endTime = participant.hasLeft ? participant.leaveTime : now; + auto duration = std::chrono::duration_cast( + endTime - participant.joinTime + ); + totalMinutes += duration.count(); + } + + return totalMinutes; + } +}; +``` + +## Meeting SDK Usage Tracking + +### Reports API for Past Meetings + +```javascript +// Get meeting usage from Reports API +async function getMeetingUsage(meetingId) { + // Note: Double-encode UUID if it contains / or // + const encodedId = meetingId.includes('/') + ? encodeURIComponent(encodeURIComponent(meetingId)) + : meetingId; + + const [meeting, participants] = await Promise.all([ + axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ), + axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ) + ]); + + // Calculate participant-minutes + const participantMinutes = participants.data.participants.map(p => { + const duration = p.duration; // Already in seconds + return { + name: p.name, + email: p.user_email, + durationMinutes: Math.round(duration / 60 * 100) / 100 + }; + }); + + const totalParticipantMinutes = participantMinutes.reduce( + (sum, p) => sum + p.durationMinutes, 0 + ); + + return { + meetingId: meetingId, + topic: meeting.data.topic, + startTime: meeting.data.start_time, + endTime: meeting.data.end_time, + hostMinutes: meeting.data.duration, // Meeting duration in minutes + totalParticipantMinutes: Math.round(totalParticipantMinutes), + participantCount: participants.data.participants.length, + participants: participantMinutes + }; +} + +// Get daily usage summary +async function getDailyUsageSummary(year, month) { + const response = await axios.get( + 'https://api.zoom.us/v2/report/daily', + { + params: { year, month }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Aggregate daily data + const summary = response.data.dates.reduce((acc, day) => { + acc.totalMeetings += day.meetings; + acc.totalMinutes += day.meeting_minutes; + acc.totalParticipants += day.participants; + return acc; + }, { totalMeetings: 0, totalMinutes: 0, totalParticipants: 0 }); + + return { + period: `${year}-${String(month).padStart(2, '0')}`, + ...summary, + dailyBreakdown: response.data.dates + }; +} + +// Get user-specific meeting report +async function getUserMeetingReport(userId, from, to) { + const response = await axios.get( + `https://api.zoom.us/v2/report/users/${userId}/meetings`, + { + params: { from, to, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const totalMinutes = response.data.meetings.reduce( + (sum, m) => sum + m.duration, 0 + ); + + return { + userId, + period: { from, to }, + totalMeetings: response.data.meetings.length, + totalHostMinutes: totalMinutes, + meetings: response.data.meetings + }; +} +``` + +### Webhook-Based Tracking + +```javascript +// Meeting SDK webhook events +app.post('/meeting-webhook', express.json(), (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'meeting.started': + handleMeetingStarted(payload); + break; + case 'meeting.ended': + handleMeetingEnded(payload); + break; + case 'meeting.participant_joined': + handleMeetingParticipantJoined(payload); + break; + case 'meeting.participant_left': + handleMeetingParticipantLeft(payload); + break; + } + + res.status(200).send(); +}); + +// Store in database for billing +async function handleMeetingEnded(payload) { + const { object } = payload; + + await db.meetings.insert({ + meetingId: object.id, + uuid: object.uuid, + topic: object.topic, + hostId: object.host_id, + startTime: object.start_time, + endTime: object.end_time, + durationMinutes: object.duration, + participantCount: object.participant_count + }); +} +``` + +## Cost Estimation + +### Video SDK Cost Calculator + +```javascript +// Pricing tiers (example - check current Zoom pricing) +const PRICING_TIERS = [ + { upTo: 10000, pricePerMinute: 0.0050 }, + { upTo: 50000, pricePerMinute: 0.0040 }, + { upTo: 100000, pricePerMinute: 0.0030 }, + { upTo: Infinity, pricePerMinute: 0.0025 } +]; + +function estimateMonthlyCost(participantMinutes) { + let remaining = participantMinutes; + let totalCost = 0; + let previousLimit = 0; + + for (const tier of PRICING_TIERS) { + const tierMinutes = Math.min(remaining, tier.upTo - previousLimit); + if (tierMinutes <= 0) break; + + totalCost += tierMinutes * tier.pricePerMinute; + remaining -= tierMinutes; + previousLimit = tier.upTo; + } + + return { + participantMinutes, + estimatedCost: Math.round(totalCost * 100) / 100, + currency: 'USD' + }; +} + +// Project usage for the month +function projectMonthlyUsage(currentUsage, dayOfMonth, daysInMonth) { + const dailyAverage = currentUsage / dayOfMonth; + const projectedTotal = dailyAverage * daysInMonth; + + return { + currentUsage, + dailyAverage: Math.round(dailyAverage), + projectedMonthlyUsage: Math.round(projectedTotal), + projectedCost: estimateMonthlyCost(projectedTotal) + }; +} +``` + +### Year-to-Date (YTD) Usage + +Calculate cumulative usage from the start of the year. + +```javascript +// Calculate YTD usage for Video SDK +async function getYTDUsage() { + const now = new Date(); + const year = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Fetch all months in parallel + const monthPromises = []; + for (let month = 1; month <= currentMonth; month++) { + monthPromises.push(calculateMonthlyUsage(year, month)); + } + + const monthlyResults = await Promise.all(monthPromises); + + // Aggregate YTD totals + const ytdTotals = monthlyResults.reduce((acc, month) => { + acc.totalSessions += month.totalSessions; + acc.totalParticipantMinutes += month.totalParticipantMinutes; + return acc; + }, { totalSessions: 0, totalParticipantMinutes: 0 }); + + // Monthly breakdown for trending + const monthlyBreakdown = monthlyResults.map(m => ({ + period: m.period, + sessions: m.totalSessions, + participantMinutes: m.totalParticipantMinutes + })); + + // Calculate month-over-month growth + const growthRates = []; + for (let i = 1; i < monthlyBreakdown.length; i++) { + const prev = monthlyBreakdown[i - 1].participantMinutes; + const curr = monthlyBreakdown[i].participantMinutes; + growthRates.push({ + period: monthlyBreakdown[i].period, + growthRate: prev > 0 ? ((curr - prev) / prev * 100).toFixed(1) + '%' : 'N/A' + }); + } + + return { + year, + asOfDate: now.toISOString().split('T')[0], + ytdTotals: { + totalSessions: ytdTotals.totalSessions, + totalParticipantMinutes: Math.round(ytdTotals.totalParticipantMinutes), + estimatedCost: estimateMonthlyCost(ytdTotals.totalParticipantMinutes) + }, + monthlyBreakdown, + growthRates, + averageMonthlyUsage: Math.round(ytdTotals.totalParticipantMinutes / currentMonth) + }; +} + +// Calculate YTD usage for Meeting SDK (via Reports API) +async function getMeetingSDKYTDUsage() { + const now = new Date(); + const year = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Fetch daily reports for each month + const monthPromises = []; + for (let month = 1; month <= currentMonth; month++) { + monthPromises.push(getDailyUsageSummary(year, month)); + } + + const monthlyResults = await Promise.all(monthPromises); + + // Aggregate YTD + const ytdTotals = monthlyResults.reduce((acc, month) => { + acc.totalMeetings += month.totalMeetings; + acc.totalMinutes += month.totalMinutes; + acc.totalParticipants += month.totalParticipants; + return acc; + }, { totalMeetings: 0, totalMinutes: 0, totalParticipants: 0 }); + + return { + year, + asOfDate: now.toISOString().split('T')[0], + ytdTotals, + monthlyBreakdown: monthlyResults.map(m => ({ + period: m.period, + meetings: m.totalMeetings, + minutes: m.totalMinutes, + participants: m.totalParticipants + })), + averageMonthly: { + meetings: Math.round(ytdTotals.totalMeetings / currentMonth), + minutes: Math.round(ytdTotals.totalMinutes / currentMonth), + participants: Math.round(ytdTotals.totalParticipants / currentMonth) + } + }; +} + +// Compare YTD to previous year +async function getYTDComparison() { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Get current YTD + const currentYTD = await getYTDUsage(); + + // Get same period last year (Jan through current month) + const lastYearPromises = []; + for (let month = 1; month <= currentMonth; month++) { + lastYearPromises.push(calculateMonthlyUsage(currentYear - 1, month)); + } + + const lastYearResults = await Promise.all(lastYearPromises); + const lastYearTotal = lastYearResults.reduce( + (sum, m) => sum + m.totalParticipantMinutes, 0 + ); + + const yearOverYearChange = lastYearTotal > 0 + ? ((currentYTD.ytdTotals.totalParticipantMinutes - lastYearTotal) / lastYearTotal * 100) + : null; + + return { + currentYear: { + year: currentYear, + totalParticipantMinutes: currentYTD.ytdTotals.totalParticipantMinutes + }, + previousYear: { + year: currentYear - 1, + totalParticipantMinutes: Math.round(lastYearTotal), + note: `Same period (Jan-${currentMonth})` + }, + yearOverYearChange: yearOverYearChange !== null + ? yearOverYearChange.toFixed(1) + '%' + : 'N/A (no prior year data)' + }; +} + +// Project full year usage based on YTD +function projectAnnualUsage(ytdMinutes, currentMonth) { + const monthsRemaining = 12 - currentMonth; + const monthlyAverage = ytdMinutes / currentMonth; + const projectedRemaining = monthlyAverage * monthsRemaining; + const projectedAnnual = ytdMinutes + projectedRemaining; + + return { + ytdMinutes: Math.round(ytdMinutes), + projectedAnnualMinutes: Math.round(projectedAnnual), + projectedAnnualCost: estimateMonthlyCost(projectedAnnual), + monthlyAverage: Math.round(monthlyAverage), + confidence: currentMonth >= 6 ? 'high' : currentMonth >= 3 ? 'medium' : 'low' + }; +} +``` + +### Usage Dashboard + +```javascript +// Build a usage dashboard +async function getUsageDashboard() { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const dayOfMonth = now.getDate(); + const daysInMonth = new Date(year, month, 0).getDate(); + + // Get current month usage + const usage = await calculateMonthlyUsage(year, month); + + // Calculate projections + const projection = projectMonthlyUsage( + usage.totalParticipantMinutes, + dayOfMonth, + daysInMonth + ); + + // Get previous month for comparison + const prevMonth = month === 1 ? 12 : month - 1; + const prevYear = month === 1 ? year - 1 : year; + const previousUsage = await calculateMonthlyUsage(prevYear, prevMonth); + + return { + currentMonth: { + period: usage.period, + totalSessions: usage.totalSessions, + totalParticipantMinutes: usage.totalParticipantMinutes, + estimatedCost: estimateMonthlyCost(usage.totalParticipantMinutes) + }, + projection: { + projectedMinutes: projection.projectedMonthlyUsage, + projectedCost: projection.projectedCost, + dailyAverage: projection.dailyAverage + }, + previousMonth: { + period: previousUsage.period, + totalParticipantMinutes: previousUsage.totalParticipantMinutes, + monthOverMonthChange: ( + (usage.totalParticipantMinutes - previousUsage.totalParticipantMinutes) / + previousUsage.totalParticipantMinutes * 100 + ).toFixed(1) + '%' + }, + topSessions: usage.sessions + .sort((a, b) => b.participantMinutes - a.participantMinutes) + .slice(0, 10) + }; +} +``` + +## Best Practices + +### Accurate Tracking + +1. **Use webhooks for real-time**: More accurate than periodic API polling +2. **Handle reconnections**: Participants may disconnect and rejoin +3. **Account for time zones**: Store all times in UTC +4. **Deduplicate participants**: Same user may rejoin multiple times + +### Cost Optimization + +1. **Monitor daily usage**: Set up alerts for unusual spikes +2. **Track by use case**: Identify which features consume most minutes +3. **Optimize session duration**: Encourage efficient meetings +4. **Review participant patterns**: Identify inactive participants + +### Data Retention + +| Data Source | Retention Period | +|-------------|------------------| +| Session Quality API | 30 days | +| Reports API (meetings) | 12 months | +| Reports API (participants) | 1 month | +| Webhook events | Store your own | + +## Related Resources + +- **Video SDK Session Quality**: https://developers.zoom.us/docs/video-sdk/session-quality/ +- **Reports API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports +- **Usage reporting guide**: See `usage-reporting-analytics.md` +- **Webhooks reference**: See `webhooks` skill diff --git a/partner-built/zoom-plugin/skills/general/use-cases/native-meeting-sdk-multi-platform.md b/partner-built/zoom-plugin/skills/general/use-cases/native-meeting-sdk-multi-platform.md new file mode 100644 index 00000000..446555b0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/native-meeting-sdk-multi-platform.md @@ -0,0 +1,34 @@ +# Native Meeting SDK Multi-Platform Delivery + +Use this flow when you need the same embedded meeting product capability across multiple native stacks (Android, iOS, macOS, Unreal) while keeping behavior and auth patterns consistent. + +## When to Use + +- You are shipping a shared meeting feature set across multiple native clients. +- You need consistent auth/signature and role policy across platforms. +- You need version-drift guardrails and staged rollout checks per platform. + +## Skill Chain + +1. [meeting-sdk](../../meeting-sdk/SKILL.md) +2. [meeting-sdk/android](../../meeting-sdk/android/SKILL.md) +3. [meeting-sdk/ios](../../meeting-sdk/ios/SKILL.md) +4. [meeting-sdk/macos](../../meeting-sdk/macos/SKILL.md) +5. [meeting-sdk/unreal](../../meeting-sdk/unreal/SKILL.md) +6. [oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Standardize server-side signing and role policy once. +2. Validate default UI join/start baseline on each platform. +3. Add platform-specific custom UI features only after baseline parity. +4. Maintain a version matrix and run upgrade checks per platform before release. +5. Track contradictions between wrapper docs, package artifacts, and API references. + +## References + +- [Meeting SDK Root Skill](../../meeting-sdk/SKILL.md) +- [Android Reference Map](../../meeting-sdk/android/references/android-reference-map.md) +- [iOS Reference Map](../../meeting-sdk/ios/references/ios-reference-map.md) +- [macOS Reference Map](../../meeting-sdk/macos/references/macos-reference-map.md) +- [Unreal Reference Map](../../meeting-sdk/unreal/references/unreal-reference-map.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/native-video-sdk-multi-platform.md b/partner-built/zoom-plugin/skills/general/use-cases/native-video-sdk-multi-platform.md new file mode 100644 index 00000000..7f853868 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/native-video-sdk-multi-platform.md @@ -0,0 +1,35 @@ +# Native Video SDK Multi-Platform Delivery + +## Goal + +Ship one product experience across Android, iOS, macOS, and Unity while keeping token auth, session behavior, and upgrade policies aligned. + +## Skills to chain + +- [zoom-video-sdk](../../video-sdk/SKILL.md) +- [zoom-video-sdk-android](../../video-sdk/android/SKILL.md) +- [zoom-video-sdk-ios](../../video-sdk/ios/SKILL.md) +- [zoom-video-sdk-macos](../../video-sdk/macos/SKILL.md) +- [zoom-video-sdk-unity](../../video-sdk/unity/SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) + +## Recommended delivery model + +1. Standardize backend token service contract (`sessionName`, `userName`, role/claims). +2. Keep platform session state machines consistent (init -> join -> media -> leave). +3. Version-lock each platform release and run compatibility checks before rollout. +4. Maintain per-platform fallback plans for renamed/deprecated APIs. + +## Failure modes to pre-plan + +- Wrapper/native feature mismatch (especially Unity). +- Event naming drift across SDK versions. +- Token claim changes that break only one platform. +- Permission/regression differences per OS release. + +## Output checklist + +- Shared auth/token contract spec +- Platform-specific session lifecycle docs +- Upgrade runbook with rollback plan +- Known incompatibility matrix diff --git a/partner-built/zoom-plugin/skills/general/use-cases/prebuilt-video-ui.md b/partner-built/zoom-plugin/skills/general/use-cases/prebuilt-video-ui.md new file mode 100644 index 00000000..54367947 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/prebuilt-video-ui.md @@ -0,0 +1,307 @@ +# Pre-built Video UI with UI Toolkit + +Build video conferencing apps in minutes using Zoom's ready-made UI components. + +## Use Case + +You need to add video conferencing to your web application quickly without building custom UI from scratch. The Zoom Video SDK UI Toolkit provides a complete, production-ready video interface that works across frameworks. + +## When to Use UI Toolkit + +- ✅ Need video conferencing fast (hours, not weeks) +- ✅ Want Zoom-like UI consistency +- ✅ Don't have resources to build custom video UI +- ✅ Need standard features (chat, share, participants, settings) +- ✅ Want framework-agnostic solution (React, Vue, Angular, vanilla JS) + +## When NOT to Use (Use Raw Video SDK Instead) + +- ❌ Need complete custom UI control +- ❌ Building non-standard video experiences +- ❌ Need access to raw video/audio data for processing +- ❌ Want custom rendering pipeline + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Web Application │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Your Frontend (React/Vue/Angular/Vanilla JS) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ Zoom UI Toolkit │ │ │ +│ │ │ ┌────────────────────────────────────────┐ │ │ │ +│ │ │ │ Pre-built UI Components │ │ │ │ +│ │ │ │ • Video Grid/Gallery │ │ │ │ +│ │ │ │ • Control Bar │ │ │ │ +│ │ │ │ • Chat Panel │ │ │ │ +│ │ │ │ • Participants List │ │ │ │ +│ │ │ │ • Settings Panel │ │ │ │ +│ │ │ └────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────────────────────────────────────┐ │ │ │ +│ │ │ │ Zoom Video SDK (Underlying Engine) │ │ │ │ +│ │ │ │ • WebRTC │ │ │ │ +│ │ │ │ • Media Processing │ │ │ │ +│ │ │ │ • Session Management │ │ │ │ +│ │ │ └────────────────────────────────────────┘ │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Your Backend (Node.js/Python/Any) │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ JWT Generation Endpoint │ │ │ +│ │ │ • Uses Video SDK Secret (NEVER expose!) │ │ │ +│ │ │ • Generates session tokens │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Implementation + +### 1. Install UI Toolkit + +```bash +npm install @zoom/videosdk-zoom-ui-toolkit +npm install react@18 react-dom@18 # Required peer dependency +``` + +### 2. Server-Side JWT Generation (Required) + +```typescript +// Backend: api/zoom-token/route.ts +import { KJUR } from 'jsrsasign'; + +export async function POST(request) { + const { sessionName, role, userName } = await request.json(); + + const payload = { + app_key: process.env.ZOOM_VIDEO_SDK_KEY, + role_type: role, // 0 = participant, 1 = host + tpc: sessionName, + version: 1, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7200 // 2 hours + }; + + const token = KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify(payload), + process.env.ZOOM_VIDEO_SDK_SECRET + ); + + return Response.json({ signature: token }); +} +``` + +### 3. Frontend Integration (React Example) + +```typescript +'use client'; +import { useEffect, useRef } from 'react'; + +export default function VideoSession({ sessionName, userName }) { + const containerRef = useRef(null); + const uitoolkitRef = useRef(null); + + useEffect(() => { + let mounted = true; + + const init = async () => { + // Fetch JWT from your backend + const response = await fetch('/api/zoom-token', { + method: 'POST', + body: JSON.stringify({ sessionName, userName, role: 1 }) + }); + const { signature } = await response.json(); + + // Import UI Toolkit + const uitoolkitModule = await import('@zoom/videosdk-zoom-ui-toolkit'); + const uitoolkit = uitoolkitModule.default; + uitoolkitRef.current = uitoolkit; + + // @ts-ignore + await import('@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css'); + + if (!mounted || !containerRef.current) return; + + // Configure session + const config = { + videoSDKJWT: signature, + sessionName, + userName, + featuresOptions: { + video: { enable: true }, + audio: { enable: true }, + share: { enable: true }, + chat: { enable: true }, + users: { enable: true }, + settings: { enable: true } + } + }; + + // Join session + uitoolkit.joinSession(containerRef.current, config); + + uitoolkit.onSessionJoined(() => console.log('Joined')); + uitoolkit.onSessionClosed(() => console.log('Closed')); + }; + + init(); + + return () => { + mounted = false; + if (uitoolkitRef.current && containerRef.current) { + uitoolkitRef.current.closeSession(containerRef.current); + uitoolkitRef.current.destroy(); + } + }; + }, [sessionName, userName]); + + return
; +} +``` + +That's it! You now have a fully functional video conferencing UI. + +## Features You Get Out-of-the-Box + +| Feature | Description | +|---------|-------------| +| **Video Grid** | Gallery and speaker views with automatic switching | +| **Audio Controls** | Mute/unmute, device selection, background noise suppression | +| **Video Controls** | Camera on/off, device selection, virtual backgrounds | +| **Screen Share** | Share screen/window with annotation support | +| **Chat** | In-session messaging with emoji support | +| **Participants** | User list with host controls (mute, remove, etc.) | +| **Settings** | Device management, quality statistics, theme selection | +| **Reactions** | Emoji reactions and raised hand | + +## Customization Options + +### Choose Which Features to Enable + +```javascript +const config = { + // ... other config + featuresOptions: { + preview: { enable: true }, // Pre-join device check + video: { enable: true }, + audio: { enable: true }, + share: { enable: true }, + chat: { enable: true }, + users: { enable: true }, + settings: { enable: true }, + virtualBackground: { + enable: true, + virtualBackgrounds: [ + { url: '/bg1.jpg', displayName: 'Office' } + ] + }, + recording: { enable: false }, // Requires paid plan + caption: { enable: false }, // Requires paid plan + theme: { + enable: true, + defaultTheme: 'dark' // 'light' | 'dark' | 'blue' | 'green' + } + } +}; +``` + +### Two UI Modes + +**Composite Mode** (Full UI - Easiest): +```javascript +// Single call gets you complete video UI +uitoolkit.joinSession(container, config); +``` + +**Component Mode** (Custom Layouts): +```javascript +// Show individual pieces where you want +uitoolkit.joinSession(container, config); +uitoolkit.showControlsComponent(controlsContainer); +uitoolkit.showChatComponent(chatContainer); +uitoolkit.showUsersComponent(usersContainer); +``` + +## Related Use Cases + +- **[Custom Video Experiences](custom-video.md)** - When you need raw Video SDK for custom UI +- **[Meeting SDK Integration](embed-meetings.md)** - For Zoom Meeting embedding +- **[Real-Time Media Streams](real-time-media-streams.md)** - When you need raw media access + +## Related Skills + +- **[zoom-ui-toolkit](../../ui-toolkit/SKILL.md)** - Complete UI Toolkit documentation +- **[zoom-video-sdk](../../video-sdk/web/SKILL.md)** - Raw Video SDK (when UI Toolkit isn't enough) +- **[zoom-general](../SKILL.md)** - General Zoom platform knowledge + +## Security Best Practices + +1. **NEVER expose Video SDK Secret** in frontend code +2. **ALWAYS generate JWT server-side** using the secret +3. **Set appropriate JWT expiration** (1-2 hours typical) +4. **Validate user identity** before generating tokens +5. **Use HTTPS** for production deployments + +## Production Checklist + +- [ ] JWT generation is server-side only +- [ ] Proper cleanup on component unmount (`uitoolkit.destroy()`) +- [ ] Error handling for network issues +- [ ] Loading states during JWT fetch +- [ ] Testing across browsers (Chrome, Firefox, Safari, Edge) +- [ ] Mobile responsive testing (if targeting mobile) +- [ ] HTTPS enabled for production +- [ ] Environment variables for SDK credentials + +## Development Time Comparison + +| Approach | Development Time | Effort | +|----------|-----------------|--------| +| **UI Toolkit** | 1-3 days | Low - Drop-in solution | +| **Raw Video SDK** | 2-4 weeks | High - Build all UI | +| **Meeting SDK** | 3-5 days | Medium - Embed Zoom Meetings | + +## When You Outgrow UI Toolkit + +If you need more customization than UI Toolkit provides: + +1. **Access underlying SDK**: + ```javascript + const client = uitoolkit.getClient(); // Get raw Video SDK client + uitoolkit.on('user-added', (payload) => { + // Listen to 80+ raw SDK events + }); + ``` + +2. **Migrate to raw Video SDK**: + - Keep your JWT generation + - Replace UI Toolkit with custom UI + - Use same session/token architecture + - See [zoom-video-sdk](../../video-sdk/web/SKILL.md) for migration guide + +## Common Gotchas + +1. **React 18 Required**: UI Toolkit needs React 18 specifically (not 17 or 19) +2. **CSS Import**: Must import `videosdk-zoom-ui-toolkit.css` or UI will be unstyled +3. **Cleanup Required**: Always call `destroy()` on unmount to prevent memory leaks +4. **JWT Security**: NEVER put SDK secret in frontend - always use server endpoint + +## Resources + +- **Skill**: [zoom-ui-toolkit](../../ui-toolkit/SKILL.md) +- **Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **Sample Apps**: + - React: https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample + - Vue.js: https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample + - Angular: https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample + - JavaScript: https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample diff --git a/partner-built/zoom-plugin/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md b/partner-built/zoom-plugin/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md new file mode 100644 index 00000000..55e19fbc --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md @@ -0,0 +1,34 @@ +# Probe SDK Preflight Readiness Gate + +Use Probe SDK before user join/start flows to detect device, browser, and network issues early. + +## When to Use + +- You want to reduce failed joins and low-quality first-minute experiences. +- You need a clear pass/warn/fail decision before launching Meeting SDK or Video SDK UX. +- You need structured diagnostics for support workflows. + +## Skill Chain + +- [probe-sdk](../../probe-sdk/SKILL.md) +- [zoom-meeting-sdk/web](../../meeting-sdk/web/SKILL.md) or [zoom-video-sdk/web](../../video-sdk/web/SKILL.md) +- [zoom-general](../SKILL.md) + +## High-Level Flow + +1. Request media permissions and enumerate devices. +2. Run targeted diagnostics for selected mic/camera/speaker. +3. Run comprehensive network probe and collect final report. +4. Apply readiness policy (`allow`, `warn`, `block`) and present next steps. +5. Launch meeting/session only when policy permits. + +## Risks + +- Renderer and report schema drift across versions. +- Browser support changes over time. +- Incomplete cleanup causing residual device/network usage. + +## See Also + +- [probe-sdk runbook](../../probe-sdk/RUNBOOK.md) +- [probe-sdk troubleshooting](../../probe-sdk/troubleshooting/common-issues.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/qss-monitoring.md b/partner-built/zoom-plugin/skills/general/use-cases/qss-monitoring.md new file mode 100644 index 00000000..f1d36e56 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/qss-monitoring.md @@ -0,0 +1,112 @@ +# QSS Monitoring + +Quality of Service Subscription for real-time meeting quality monitoring. + +## Overview + +QSS (Quality of Service Subscription) provides near real-time QoS telemetry for Zoom Meetings, Webinars, and Phone. IT teams can monitor network quality, diagnose issues, and track performance at scale. + +## Skills Needed + +- **webhooks** - Receive QSS events +- **zoom-rest-api** - QSS API endpoints + +## What QSS Provides + +### Quality Metrics + +| Metric | Description | +|--------|-------------| +| Bitrate | Data transfer rate | +| Latency | Network delay | +| Jitter | Latency variation | +| Packet Loss | Lost data packets | +| Resolution | Video resolution | +| Frame Rate | Video FPS | +| CPU Usage | Client CPU load | + +### Usage Metrics + +| Metric | Description | +|--------|-------------| +| Device | Device type and model | +| Network | Network type and quality | +| Signaling Region | Connection region | +| Client Version | Zoom client version | +| Audio I/O | Audio device info | +| Video I/O | Camera info | + +## Data Delivery + +| Aspect | Details | +|--------|---------| +| **Frequency** | ~1 event per minute per participant | +| **Delivery** | Webhook events | +| **Retention** | 7 days via Webhook Logs API | +| **Products** | Meetings, Webinars, Phone | + +## Prerequisites + +- Business or Enterprise Zoom account +- QSS add-on subscription +- Webhook endpoint configured +- Internal dashboard system (recommended) + +## Setup + +### 1. Enable QSS + +Contact Zoom sales to add QSS to your account. + +### 2. Configure Webhooks + +Subscribe to QSS events in your app's Event Subscriptions. + +### 3. Handle Events + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event.startsWith('qss.')) { + // Process QSS data + const { meeting_id, participant_id, metrics } = payload; + + // Send to your monitoring dashboard + dashboard.ingest({ + meetingId: meeting_id, + participantId: participant_id, + bitrate: metrics.bitrate, + latency: metrics.latency, + jitter: metrics.jitter, + packetLoss: metrics.packet_loss + }); + } + + res.status(200).send(); +}); +``` + +## Use Cases + +| Use Case | Description | +|----------|-------------| +| **Network monitoring** | Track network quality across organization | +| **Troubleshooting** | Diagnose call quality issues in real-time | +| **Capacity planning** | Understand bandwidth usage patterns | +| **SLA compliance** | Monitor meeting quality for SLA reporting | +| **Proactive alerts** | Alert IT when quality degrades | + +## Integration + +QSS data can be integrated with: +- Splunk +- Datadog +- Grafana +- Custom dashboards +- ITSM tools + +## Resources + +- **QSS docs**: https://developers.zoom.us/docs/api/rest/qss-api/ +- **QSS API reference**: https://developers.zoom.us/docs/api/rest/reference/qss/methods/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/raw-recording.md b/partner-built/zoom-plugin/skills/general/use-cases/raw-recording.md new file mode 100644 index 00000000..2ba90bac --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/raw-recording.md @@ -0,0 +1,172 @@ +# Raw Recording + +Access raw audio and video data from Zoom meetings and sessions for custom processing. + +## Overview + +Raw recording allows you to capture unprocessed audio and video frames directly from the SDK, enabling custom recording solutions, AI processing, and media pipelines. + +## Platform Support + +| Platform | Support Level | SDK | +|----------|---------------|-----| +| **Linux** | Primary | Meeting SDK, Video SDK | +| **Windows** | Primary | Meeting SDK, Video SDK | +| **macOS** | Primary | Meeting SDK, Video SDK | +| **iOS** | Light | Meeting SDK, Video SDK | +| **Android** | Light | Meeting SDK, Video SDK | +| **Web** | No native support | Use 3rd party browser recording; must call recording API for compliance notification | + +## Desktop Platforms (Primary) + +### Linux + +```cpp +// Subscribe to raw audio +class AudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData *data) override { + // Format: 16-bit PCM, 16kHz or 32kHz, mono + std::ofstream file("meeting.pcm", std::ios::binary | std::ios::app); + file.write(data->GetBuffer(), data->GetBufferLen()); + } +}; + +// Subscribe to raw video +class VideoRawDataDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420 *data) override { + // Format: I420 (YUV 4:2:0) - contiguous planar data + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Write raw YUV to file (can convert with ffmpeg later) + yuvFile.write(data->GetYBuffer(), width * height); + yuvFile.write(data->GetUBuffer(), (width/2) * (height/2)); + yuvFile.write(data->GetVBuffer(), (width/2) * (height/2)); + } +}; + +// Enable raw recording +auto recCtl = m_meetingService->GetMeetingRecordingController(); +recCtl->StartRawRecording(); + +// Subscribe +GetAudioRawdataHelper()->subscribe(new AudioRawDataDelegate()); +GetRawdataRendererHelper()->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +**Playing/Converting Raw Files:** +```bash +# Play raw YUV video (adjust dimensions to match output) +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv + +# Convert YUV to MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 + +# Play raw PCM audio +ffplay -f s16le -ar 32000 -ac 1 audio.pcm + +# Combine video + audio +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +### Windows + +```cpp +// Same API as Linux +// Subscribe to raw data after joining meeting +auto* pRawDataHelper = GetAudioRawdataHelper(); +pRawDataHelper->subscribe(new AudioRawDataDelegate()); + +auto* pVideoHelper = GetRawdataRendererHelper(); +pVideoHelper->setRawDataResolution(ZoomSDKResolution_720P); +pVideoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +### macOS + +```swift +// Get raw data controller +let rawDataCtrl = ZoomSDK.shared().getRawDataController() + +// Subscribe to audio +rawDataCtrl?.subscribeAudioRawData { audioData in + // Process PCM audio + let buffer = audioData.getBuffer() + let length = audioData.getBufferLen() +} + +// Subscribe to video +rawDataCtrl?.subscribeVideoRawData(forUser: userId) { videoData in + // Process YUV frames + let width = videoData.getStreamWidth() + let height = videoData.getStreamHeight() +} +``` + +## Mobile Platforms (Light) + +### iOS + +Raw data access on iOS is more limited than desktop: + +```swift +// Video raw data via ZoomVideoSDKVideoCanvas +let canvas = ZoomVideoSDKVideoCanvas() +canvas.delegate = self + +// Implement delegate +func onRawDataFrameReceived(_ pixelBuffer: CVPixelBuffer) { + // Process video frame + // Note: Performance-intensive on mobile +} +``` + +**Limitations:** +- Higher battery consumption +- May impact app performance +- Not recommended for long recordings + +### Android + +```kotlin +// Video raw data via ZoomVideoSDKVideoCanvas +val canvas = ZoomVideoSDKVideoCanvas(context) +canvas.setDelegate(object : ZoomVideoSDKRawDataPipeDelegate { + override fun onRawDataFrameReceived(rawData: ZoomVideoSDKVideoRawData) { + // Process YUV frame + // Note: Performance-intensive on mobile + } +}) +``` + +**Limitations:** +- Same as iOS - battery and performance concerns +- Consider cloud recording instead for mobile apps + +## Web Platform + +There is no native SDK support for raw recording on Web. For browser-based recording: + +1. Use 3rd party browser recording solutions +2. **Important**: Call the recording API to trigger the "This meeting is being recorded" notification for compliance + +## Common Use Cases + +- Meeting bots for transcription +- Custom recording pipelines +- AI/ML processing (sentiment analysis, summarization) +- Media archival solutions + +## Detailed Platform Guides + +- **[Video SDK Linux Guide](../../video-sdk/linux/linux.md)** - Complete C++ implementation for headless bots +- **[Meeting SDK Linux Guide](../../meeting-sdk/linux/linux.md)** - Meeting SDK raw data capture + +## Resources + +- **Meeting SDK docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Video SDK docs**: https://developers.zoom.us/docs/video-sdk/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/react-native-meeting-embed.md b/partner-built/zoom-plugin/skills/general/use-cases/react-native-meeting-embed.md new file mode 100644 index 00000000..a19048e7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/react-native-meeting-embed.md @@ -0,0 +1,27 @@ +# React Native Meeting Embed + +Use this flow when you need Zoom meetings inside a mobile app built with React Native. + +## When to Use + +- iOS/Android app already uses React Native +- You need Zoom meeting join/start inside app navigation +- You want Zoom Meeting SDK UI from React Native wrapper, not a custom video stack + +## Skill Chain + +1. **[meeting-sdk/react-native](../../meeting-sdk/react-native/SKILL.md)** for wrapper APIs and platform setup +2. **[zoom-oauth](../../oauth/SKILL.md)** for backend token handling (SDK JWT and ZAK) + +## Typical Flow + +1. Backend issues SDK JWT for mobile client. +2. App initializes SDK with `initSDK`. +3. App joins by `joinMeeting` (attendee) or starts by `startMeeting` (host + ZAK). +4. App handles meeting lifecycle and calls `cleanup` during teardown. + +## References + +- [Meeting SDK React Native Skill](../../meeting-sdk/react-native/SKILL.md) +- [Auth and Token Model](../../meeting-sdk/react-native/concepts/auth-and-token-model.md) +- [Join Meeting Pattern](../../meeting-sdk/react-native/examples/join-meeting-pattern.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/react-native-video-sessions.md b/partner-built/zoom-plugin/skills/general/use-cases/react-native-video-sessions.md new file mode 100644 index 00000000..507fd1c7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/react-native-video-sessions.md @@ -0,0 +1,28 @@ +# React Native Video Sessions + +Use this flow for custom mobile video session products in React Native. + +## When to Use + +- You need full custom UX (not Zoom Meeting UI). +- You are building iOS/Android apps using `@zoom/react-native-videosdk`. +- You need helper-based features such as chat/share/recording/transcription. + +## Skill Chain + +1. [video-sdk/react-native](../../video-sdk/react-native/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Video SDK JWT. +2. App initializes SDK provider and listeners. +3. App joins session with tokenized config. +4. App drives helper APIs and event-based UI state. +5. App leaves session and cleans up resources. + +## References + +- [React Native Video SDK Skill](../../video-sdk/react-native/SKILL.md) +- [Lifecycle Workflow](../../video-sdk/react-native/concepts/lifecycle-workflow.md) +- [Session Join Pattern](../../video-sdk/react-native/examples/session-join-pattern.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/real-time-media-streams.md b/partner-built/zoom-plugin/skills/general/use-cases/real-time-media-streams.md new file mode 100644 index 00000000..d864b068 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/real-time-media-streams.md @@ -0,0 +1,237 @@ +# Real-Time Media Streams + +Access live audio, video, transcripts, chat, and screen share from Zoom meetings via WebSocket. + +## Overview + +Zoom RTMS (Realtime Media Streams) provides WebSocket-based access to live meeting media for real-time AI processing, transcription, analysis, and recording - **without meeting bots**. + +> **See the comprehensive RTMS skill**: [rtms/SKILL.md](../../rtms/SKILL.md) + +## Skills Needed + +- **[zoom-rtms](../../rtms/SKILL.md)** - Primary (comprehensive documentation) +- **webhooks** - Receive RTMS start/stop events + +## Two Approaches + +| Approach | Best For | Documentation | +|----------|----------|---------------| +| **SDK** (`@zoom/rtms`) | Most use cases | [SDK Quickstart](../../rtms/examples/sdk-quickstart.md) | +| **Manual WebSocket** | Full protocol control | [Manual WebSocket](../../rtms/examples/manual-websocket.md) | + +## How It Works + +``` +RTMS Flow: +1. Meeting starts with RTMS enabled + ↓ +2. Webhook: meeting.rtms_started + ↓ +3. Connect to Signaling WebSocket + ↓ +4. Connect to Media WebSocket + ↓ +5. Receive live audio/video/transcript/chat/share +``` + +## Media Types + +| Type | Format | Use Case | +|------|--------|----------| +| Audio | PCM 16-bit | Transcription, analysis | +| Video | H.264 | Visual AI, recording | +| Transcript | JSON | Real-time captions | + +## Prerequisites + +- RTMS feature enabled on app +- Webhook endpoint +- WebSocket client + +## Quick Start + +```javascript +// Handle RTMS webhook +app.post('/webhook', (req, res) => { + if (req.body.event === 'meeting.rtms_started') { + const { server_urls, signature } = req.body.payload; + + // Connect to RTMS + const ws = new WebSocket(server_urls); + ws.on('message', (data) => { + // Process live media + }); + } + res.status(200).send(); +}); +``` + +## Common Tasks + +### Connecting to RTMS WebSocket + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +// Webhook handler receives RTMS connection info +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.rtms_started') { + const { server_urls, stream_id, signature } = payload; + + // Connect to RTMS WebSocket + connectToRTMS(server_urls[0], stream_id, signature); + } + + res.status(200).send(); +}); + +function connectToRTMS(url, streamId, signature) { + const ws = new WebSocket(url); + + ws.on('open', () => { + // Authenticate + ws.send(JSON.stringify({ + type: 'auth', + stream_id: streamId, + signature: signature + })); + }); + + ws.on('message', (data) => { + const message = JSON.parse(data); + handleRTMSMessage(message); + }); + + ws.on('error', (err) => { + console.error('RTMS error:', err); + // Implement reconnection logic + }); +} +``` + +### Processing Audio Streams + +```javascript +function handleRTMSMessage(message) { + switch (message.type) { + case 'audio': + processAudio(message); + break; + case 'video': + processVideo(message); + break; + case 'transcript': + processTranscript(message); + break; + } +} + +function processAudio(message) { + // Audio format: PCM 16-bit, 16kHz, mono + const audioBuffer = Buffer.from(message.data, 'base64'); + + // Send to transcription service (e.g., OpenAI Whisper, Deepgram) + transcriptionService.processChunk(audioBuffer, { + sampleRate: 16000, + channels: 1, + format: 'pcm_s16le' + }); +} +``` + +### Handling Video Frames + +```javascript +function processVideo(message) { + // Video format: H.264 encoded + const videoFrame = Buffer.from(message.data, 'base64'); + + // Decode H.264 frame (requires ffmpeg or similar) + const decodedFrame = h264Decoder.decode(videoFrame); + + // Process for AI (face detection, emotion analysis, etc.) + aiProcessor.analyzeFrame(decodedFrame, { + width: message.width, + height: message.height, + timestamp: message.timestamp + }); +} +``` + +### Real-Time Transcription Integration + +```javascript +// Using Zoom's built-in transcript stream +function processTranscript(message) { + const { text, speaker_id, timestamp, is_final } = message; + + if (is_final) { + // Final transcript segment + saveTranscript({ + speaker: speaker_id, + text: text, + timestamp: timestamp + }); + + // Optionally analyze with AI + analyzeWithAI(text); + } else { + // Partial transcript - update UI in real-time + updateLiveCaption(text); + } +} + +// Integration with external AI for summarization +async function analyzeWithAI(transcript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Extract action items from this meeting segment.' + }, { + role: 'user', + content: transcript + }] + }); + + return response.choices[0].message.content; +} +``` + +### Error Handling & Reconnection + +```javascript +class RTMSClient { + constructor(config) { + this.config = config; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + } + + connect() { + this.ws = new WebSocket(this.config.url); + + this.ws.on('close', () => { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + const delay = Math.pow(2, this.reconnectAttempts) * 1000; + setTimeout(() => this.connect(), delay); + this.reconnectAttempts++; + } + }); + + this.ws.on('open', () => { + this.reconnectAttempts = 0; + this.authenticate(); + }); + } +} +``` + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **RTMS Quick Start**: https://developers.zoom.us/docs/rtms/getting-started/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/recording-download-pipeline.md b/partner-built/zoom-plugin/skills/general/use-cases/recording-download-pipeline.md new file mode 100644 index 00000000..ec90fe5d --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/recording-download-pipeline.md @@ -0,0 +1,308 @@ +# Recording Download Pipeline + +Automatically download Zoom Meeting cloud recordings to your own storage (S3, GCS, Azure Blob, etc.) using webhooks and REST API. + +> **Note:** This is NOT Video SDK BYOS (Bring Your Own Storage). Video SDK BYOS saves recordings **directly** to S3 without downloading through Zoom servers. See `zoom-video-sdk` for true BYOS. + +## Overview + +Set up automated pipelines to download Zoom Meeting cloud recordings to your own storage infrastructure for compliance, cost management, or integration with existing media workflows. + +## Skills Needed + +- **webhooks** - Receive recording events +- **zoom-rest-api** - Download recordings + +## Architecture + +``` +Recording Download Pipeline: +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Zoom │────▶│ Webhook │────▶│ Your │ +│ Cloud │ │ Handler │ │ Storage │ +│ Recording │ │ │ │ (S3, GCS) │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +## Prerequisites + +- Cloud recording enabled +- Webhook endpoint +- Storage bucket (S3, GCS, Azure, etc.) +- `recording:read` scope + +## Workflow + +1. Meeting ends, cloud recording completes +2. Receive `recording.completed` webhook +3. Download recording via API +4. Upload to your storage +5. (Optional) Delete from Zoom cloud + +## Common Tasks + +### S3 Upload Integration + +```javascript +const axios = require('axios'); +const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); + +const s3 = new S3Client({ region: 'us-east-1' }); + +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { uuid, recording_files, host_email, topic } = payload.object; + + // Process each recording file + for (const file of recording_files) { + await uploadToS3(file, uuid, topic); + } + + // Optionally delete from Zoom after successful upload + // await deleteZoomRecording(uuid); + } + + res.status(200).send(); +}); + +async function uploadToS3(file, meetingUuid, topic) { + // Download from Zoom + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + // Sanitize topic for filename + const safeTopic = topic.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50); + const key = `recordings/${meetingUuid}/${safeTopic}_${file.file_type}.${file.file_extension}`; + + // Upload to S3 using multipart upload for large files + const upload = new Upload({ + client: s3, + params: { + Bucket: 'your-recordings-bucket', + Key: key, + Body: response.data, + ContentType: getContentType(file.file_extension), + Metadata: { + 'meeting-uuid': meetingUuid, + 'file-type': file.file_type, + 'recording-start': file.recording_start + } + } + }); + + await upload.done(); + return `s3://your-recordings-bucket/${key}`; +} +``` + +### GCS Upload Integration + +```javascript +const { Storage } = require('@google-cloud/storage'); +const storage = new Storage(); +const bucket = storage.bucket('your-recordings-bucket'); + +async function uploadToGCS(file, meetingUuid, topic) { + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + const safeTopic = topic.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50); + const filePath = `recordings/${meetingUuid}/${safeTopic}_${file.file_type}.${file.file_extension}`; + + const gcsFile = bucket.file(filePath); + + return new Promise((resolve, reject) => { + const writeStream = gcsFile.createWriteStream({ + metadata: { + contentType: getContentType(file.file_extension), + metadata: { + meetingUuid: meetingUuid, + fileType: file.file_type + } + }, + resumable: true // Important for large files + }); + + response.data.pipe(writeStream) + .on('finish', () => resolve(`gs://your-recordings-bucket/${filePath}`)) + .on('error', reject); + }); +} +``` + +### Handling Large Files + +```javascript +// Use streaming to avoid memory issues +async function streamDownload(downloadUrl, destination) { + const response = await axios({ + method: 'GET', + url: downloadUrl, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream', + maxContentLength: Infinity, // Allow large files + maxBodyLength: Infinity + }); + + // Track progress + const totalSize = parseInt(response.headers['content-length'], 10); + let downloadedSize = 0; + + response.data.on('data', (chunk) => { + downloadedSize += chunk.length; + const progress = ((downloadedSize / totalSize) * 100).toFixed(2); + console.log(`Download progress: ${progress}%`); + }); + + return response.data; +} + +// For very large files, use chunked upload +async function chunkedUploadToS3(stream, key) { + const upload = new Upload({ + client: s3, + params: { + Bucket: 'your-bucket', + Key: key, + Body: stream + }, + queueSize: 4, // Concurrent part uploads + partSize: 10 * 1024 * 1024 // 10MB parts + }); + + upload.on('httpUploadProgress', (progress) => { + console.log(`Upload progress: ${progress.loaded}/${progress.total}`); + }); + + await upload.done(); +} +``` + +### Retry Logic for Failed Downloads + +```javascript +const retry = require('async-retry'); + +async function downloadWithRetry(file, meetingUuid) { + return await retry( + async (bail, attemptNumber) => { + console.log(`Attempt ${attemptNumber} for ${file.file_type}`); + + try { + return await uploadToS3(file, meetingUuid); + } catch (error) { + // Don't retry on permanent errors + if (error.response?.status === 404) { + bail(new Error('Recording not found')); + return; + } + if (error.response?.status === 401) { + // Refresh token and retry + await refreshAccessToken(); + } + throw error; // Retry + } + }, + { + retries: 3, + factor: 2, + minTimeout: 1000, + maxTimeout: 10000, + onRetry: (error, attempt) => { + console.log(`Retry attempt ${attempt}: ${error.message}`); + } + } + ); +} + +// Queue system for processing +const Queue = require('bull'); +const recordingQueue = new Queue('recording-uploads', process.env.REDIS_URL || 'redis://YOUR_REDIS_HOST:6379'); + +recordingQueue.process(async (job) => { + const { file, meetingUuid, topic } = job.data; + return await downloadWithRetry(file, meetingUuid, topic); +}); + +// Add job with retry +app.post('/webhook', async (req, res) => { + if (req.body.event === 'recording.completed') { + const { uuid, recording_files, topic } = req.body.payload.object; + + for (const file of recording_files) { + await recordingQueue.add( + { file, meetingUuid: uuid, topic }, + { attempts: 3, backoff: { type: 'exponential', delay: 5000 }} + ); + } + } + res.status(200).send(); +}); +``` + +### Recording Lifecycle Management + +```javascript +// Delete from Zoom after successful upload +async function deleteZoomRecording(meetingUuid) { + // Double-encode UUID if it contains / or // + const encodedUuid = encodeURIComponent(encodeURIComponent(meetingUuid)); + + await axios.delete( + `https://api.zoom.us/v2/meetings/${encodedUuid}/recordings`, + { + params: { action: 'trash' }, // Move to trash (recoverable for 30 days) + // params: { action: 'delete' }, // Permanent delete + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); +} + +// Track what's been archived +const db = require('./db'); + +async function markAsArchived(meetingUuid, s3Path) { + await db.recordings.upsert({ + meeting_uuid: meetingUuid, + s3_path: s3Path, + archived_at: new Date(), + deleted_from_zoom: false + }); +} + +// Clean up old recordings from Zoom +async function cleanupOldRecordings() { + const archivedRecordings = await db.recordings.findAll({ + where: { + archived_at: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + deleted_from_zoom: false + } + }); + + for (const recording of archivedRecordings) { + await deleteZoomRecording(recording.meeting_uuid); + await db.recordings.update( + { deleted_from_zoom: true }, + { where: { meeting_uuid: recording.meeting_uuid }} + ); + } +} +``` + +## Resources + +- **Recordings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **AWS S3 SDK**: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/ +- **Google Cloud Storage**: https://cloud.google.com/storage/docs/reference/libraries diff --git a/partner-built/zoom-plugin/skills/general/use-cases/recording-transcription.md b/partner-built/zoom-plugin/skills/general/use-cases/recording-transcription.md new file mode 100644 index 00000000..3f789d91 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/recording-transcription.md @@ -0,0 +1,302 @@ +# Recording & Transcription + +Download cloud recordings and access transcripts from Zoom meetings. + +## Overview + +Access Zoom's cloud recordings and automated transcripts via webhooks and REST API for archival, compliance, or further processing. + +For deterministic archival pipelines, keep REST API + webhooks as the primary route. +For AI-agent retrieval of summaries/transcripts through tool discovery, use `zoom-mcp`. + +## Skills Needed + +- **zoom-webhooks** - Receive recording.completed events (pipeline mode) +- **zoom-rest-api** - Download recordings and transcripts (pipeline mode) +- **zoom-mcp** - AI-driven transcript/summary retrieval (agent mode) + +## Flow + +``` +Recording Flow: +1. Meeting ends + ↓ +2. Cloud recording processes + ↓ +3. Webhook: recording.completed + ↓ +4. API: Download recording files + ↓ +5. API: Get transcript (if enabled) +``` + +MCP Agent Flow: +1. Agent invokes search tool + ↓ +2. MCP: search meetings / recordings by context + ↓ +3. MCP: retrieve transcript-capable recording details + ↓ +4. Agent fetches transcript file or uses returned summary context + +## Prerequisites + +- Cloud recording enabled on account +- `recording:read` scope +- Webhook endpoint for `recording.completed` + +## Quick Start + +```javascript +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { recording_files } = payload.object; + + for (const file of recording_files) { + // Download recording + const response = await fetch(file.download_url, { + headers: { 'Authorization': `Bearer ${token}` } + }); + } + } + + res.status(200).send(); +}); +``` + +## Common Tasks + +### Downloading Video Recordings + +```javascript +const axios = require('axios'); +const fs = require('fs'); + +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { uuid, recording_files } = payload.object; + + for (const file of recording_files) { + if (file.file_type === 'MP4') { + await downloadRecording(file, uuid); + } + } + } + + res.status(200).send(); +}); + +async function downloadRecording(file, meetingUuid) { + // Download URL requires authentication + const downloadUrl = file.download_url; + + const response = await axios({ + method: 'GET', + url: downloadUrl, + headers: { + 'Authorization': `Bearer ${accessToken}` + }, + responseType: 'stream' + }); + + // Save to file + const writer = fs.createWriteStream(`recordings/${meetingUuid}.mp4`); + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); +} +``` + +### Getting Audio-Only Files + +```javascript +// Recording files include multiple formats +const audioFormats = ['M4A']; +const videoFormats = ['MP4']; +const transcriptFormats = ['TRANSCRIPT', 'VTT']; + +app.post('/webhook', async (req, res) => { + const { recording_files } = req.body.payload.object; + + for (const file of recording_files) { + switch (file.file_type) { + case 'M4A': + // Audio-only recording + await saveFile(file, 'audio'); + break; + case 'MP4': + // Video recording (includes audio) + await saveFile(file, 'video'); + break; + case 'TRANSCRIPT': + // JSON transcript + await saveFile(file, 'transcript'); + break; + case 'VTT': + // WebVTT caption file + await saveFile(file, 'captions'); + break; + } + } +}); +``` + +### Accessing VTT Transcripts + +```javascript +// VTT file comes with recording.completed webhook +async function downloadTranscript(file, meetingUuid) { + const response = await axios.get(file.download_url, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + // Parse VTT format + const vttContent = response.data; + const parsed = parseVTT(vttContent); + + return parsed; +} + +// Simple VTT parser +function parseVTT(vttContent) { + const lines = vttContent.split('\n'); + const cues = []; + let currentCue = null; + + for (const line of lines) { + if (line.includes('-->')) { + const [start, end] = line.split(' --> '); + currentCue = { start, end, text: '' }; + } else if (currentCue && line.trim()) { + currentCue.text += line + ' '; + } else if (currentCue && !line.trim()) { + cues.push(currentCue); + currentCue = null; + } + } + + return cues; +} +``` + +### Getting Transcript via API + +```javascript +// Alternative: Get transcript via REST API +async function getTranscriptFromAPI(meetingUuid) { + // Double-encode UUID if it contains '/' or '//' + const encodedUuid = encodeURIComponent(encodeURIComponent(meetingUuid)); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${encodedUuid}/recordings`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const transcriptFile = response.data.recording_files.find( + f => f.file_type === 'TRANSCRIPT' + ); + + if (transcriptFile) { + return await downloadTranscript(transcriptFile, meetingUuid); + } +} +``` + +### Handling Recording Expiration + +```javascript +// Recordings auto-delete based on account settings (30-120 days) +// Download and archive before expiration + +// Option 1: Download immediately on webhook +app.post('/webhook', async (req, res) => { + if (req.body.event === 'recording.completed') { + await archiveRecording(req.body.payload); + } +}); + +// Option 2: Batch download via scheduled job +async function downloadPendingRecordings() { + // List recordings from last 7 days + const from = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + const to = new Date().toISOString().split('T')[0]; + + const response = await axios.get( + `https://api.zoom.us/v2/users/me/recordings?from=${from}&to=${to}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + for (const meeting of response.data.meetings) { + if (!isArchived(meeting.uuid)) { + await archiveRecording(meeting); + } + } +} + +// Run daily +cron.schedule('0 0 * * *', downloadPendingRecordings); +``` + +### Upload to Cloud Storage (S3/GCS) + +```javascript +const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); + +async function uploadToS3(stream, key) { + const s3 = new S3Client({ region: 'us-east-1' }); + + const upload = new Upload({ + client: s3, + params: { + Bucket: 'zoom-recordings', + Key: key, + Body: stream, + ContentType: 'video/mp4' + } + }); + + await upload.done(); + return `s3://zoom-recordings/${key}`; +} + +// Stream directly from Zoom to S3 +async function archiveToS3(file, meetingUuid) { + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + const key = `recordings/${meetingUuid}/${file.file_type.toLowerCase()}.${file.file_extension}`; + return await uploadToS3(response.data, key); +} +``` + +## File Types Reference + +| File Type | Extension | Description | +|-----------|-----------|-------------| +| MP4 | .mp4 | Video recording (active speaker or gallery) | +| M4A | .m4a | Audio-only recording | +| CHAT | .txt | Chat messages | +| TRANSCRIPT | .json | JSON transcript with timestamps | +| VTT | .vtt | WebVTT captions file | +| TIMELINE | .json | Meeting timeline events | + +## Resources + +- **Recordings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **Recording Webhooks**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/#recording-completed diff --git a/partner-built/zoom-plugin/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md b/partner-built/zoom-plugin/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md new file mode 100644 index 00000000..0b8ed0a7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md @@ -0,0 +1,943 @@ +# Retrieve Meeting Details and Subscribe to Events + +Comprehensive guide showing all ways to retrieve meeting details and subscribe to events in Zoom. The term "subscribe to events" has multiple meanings in Zoom, each requiring different skill combinations. + +## Overview + +When someone says "retrieve meeting details and subscribe to events," they could mean: + +| Pattern | Event Type | Skills | When to Use | +|---------|------------|--------|-------------| +| **Pattern 1** | Server-side webhooks (HTTP) | zoom-rest-api → webhooks | Account-level event monitoring, backend processing | +| **Pattern 2** | Server-side WebSockets (WS) | zoom-rest-api → zoom-websockets | Low-latency events, no exposed endpoint | +| **Pattern 3** | Client-side SDK events | zoom-rest-api → zoom-meeting-sdk | In-meeting participant events, real-time UI updates | +| **Pattern 4** | Real-time media events | zoom-rest-api → rtms | Bot-based transcription, recording, AI analysis | +| **Pattern 5** | Reports API (polling) | zoom-rest-api only | Historical data, batch processing | + +## Critical Distinction: Event Subscription Scope + +**Important**: Most Zoom event subscriptions are **account-level**, not meeting-specific: + +| Subscription Type | Scope | Filtering | +|-------------------|-------|-----------| +| **Webhooks** | Account-level | Filter events by meeting ID in handler | +| **WebSockets** | Account-level | Filter events by meeting ID in handler | +| **Meeting SDK** | Meeting-specific | Only events for joined meeting | +| **RTMS** | Meeting-specific | Only events/media for specific meeting | +| **Reports API** | Account-level | Query by meeting ID | + +**You cannot dynamically subscribe to webhooks/WebSockets for a single meeting**. These are configured at app creation time in the Marketplace portal and receive events for ALL meetings in the account. Your code filters events by meeting ID. + +--- + +## Pattern 1: REST API + Webhooks (Server-Side HTTP Events) + +**When to use:** +- Backend processing of meeting lifecycle events +- Attendance tracking, post-meeting workflows +- Recording download automation +- Standard event-driven architectures + +**Skills needed:** `zoom-rest-api` → `webhooks` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE (One-time in Marketplace Portal): │ +│ │ +│ 1. Configure Event Subscriptions │ +│ └── Subscribe to: meeting.started, meeting.ended, etc. │ +│ └── Provide webhook endpoint URL │ +│ └── Get webhook secret token │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details (topic, join_url, host, etc.) │ +│ │ +│ Step 2: Store meeting ID for filtering │ +│ └── Your backend remembers which meetings to track │ +│ │ +│ Step 3: POST /webhooks/zoom (webhooks) │ +│ └── Zoom sends events for ALL meetings │ +│ └── Filter by meeting ID in your handler │ +│ └── Process: started, ended, participant events │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +```javascript +const express = require('express'); +const axios = require('axios'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// Track which meetings we care about (use Redis/DB in production) +const trackedMeetings = new Set(); + +// ============================================ +// STEP 1: Retrieve Meeting Details (zoom-rest-api) +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// API: Fetch meeting and start tracking it +app.get('/api/meetings/:meetingId', async (req, res) => { + try { + const { meetingId } = req.params; + + // Get meeting details + const meeting = await getMeetingDetails(meetingId); + + // Add to tracked set (events for this meeting will be processed) + trackedMeetings.add(String(meeting.id)); + + console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`); + + res.json({ + success: true, + meeting: { + id: meeting.id, + topic: meeting.topic, + start_time: meeting.start_time, + join_url: meeting.join_url + }, + message: 'Meeting tracked. Webhook events will be processed for this meeting.' + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + success: false, + error: error.message + }); + } +}); + +// ============================================ +// STEP 2: Handle Webhook Events (webhooks) +// ============================================ + +function verifyWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const expectedSignature = `v0=${crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(payload) + .digest('hex')}`; + + return signature === expectedSignature; +} + +app.post('/webhooks/zoom', async (req, res) => { + // Handle URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hash + }); + } + + // Verify signature + if (!verifyWebhookSignature(req)) { + console.error('❌ Invalid webhook signature'); + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + // CRITICAL: Filter events - only process tracked meetings + if (!trackedMeetings.has(meetingId)) { + console.log(`⏭️ Ignoring event for untracked meeting ${meetingId}`); + return res.status(200).send(); + } + + // Process events for tracked meetings + console.log(`📩 Event: ${event} for meeting ${meetingId}`); + + switch (event) { + case 'meeting.started': + console.log(`✅ Meeting started: ${payload.object.topic}`); + // Handle meeting start + break; + + case 'meeting.ended': + console.log(`🏁 Meeting ended: ${payload.object.topic}`); + // Handle meeting end, cleanup + trackedMeetings.delete(meetingId); + break; + + case 'meeting.participant_joined': + console.log(`👋 ${payload.object.participant.user_name} joined`); + // Track attendance + break; + + case 'meeting.participant_left': + console.log(`👋 ${payload.object.participant.user_name} left`); + // Update attendance + break; + } + + res.status(200).send(); +}); + +app.listen(3000, () => { + console.log('Server running on port 3000'); + console.log(''); + console.log('Endpoints:'); + console.log(' GET /api/meetings/:id - Fetch & track meeting'); + console.log(' POST /webhooks/zoom - Webhook receiver'); +}); +``` + +**See also**: [meeting-details-with-events.md](meeting-details-with-events.md) for expanded webhook implementation. + +--- + +## Pattern 2: REST API + WebSockets (Server-Side Low-Latency Events) + +**When to use:** +- Lower latency than webhooks required +- Security-sensitive industries (no exposed endpoint) +- Bidirectional communication needed +- Real-time dashboards, live monitoring + +**Skills needed:** `zoom-rest-api` → `zoom-websockets` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE (One-time in Marketplace Portal): │ +│ │ +│ 1. Create Server-to-Server OAuth app │ +│ 2. Configure WebSocket Event Subscription │ +│ └── Subscribe to: meeting.started, meeting.ended, etc. │ +│ └── Get subscription ID │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details │ +│ │ +│ Step 2: Connect to WebSocket (zoom-websockets) │ +│ └── wss://ws.zoom.us/ws?subscriptionId=... │ +│ └── Authenticate with S2S OAuth access token │ +│ │ +│ Step 3: Receive events via WebSocket │ +│ └── Filter by meeting ID in message handler │ +│ └── Lower latency than webhooks │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +```javascript +const axios = require('axios'); +const WebSocket = require('ws'); + +// Track which meetings we care about +const trackedMeetings = new Set(); +let ws = null; + +// ============================================ +// STEP 1: Retrieve Meeting Details (zoom-rest-api) +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data.access_token; +} + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// ============================================ +// STEP 2: Connect to WebSocket (zoom-websockets) +// ============================================ + +async function connectWebSocket() { + const accessToken = await getAccessToken(); + const subscriptionId = process.env.ZOOM_WEBSOCKET_SUBSCRIPTION_ID; + + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${subscriptionId}&access_token=${accessToken}`; + + ws = new WebSocket(wsUrl); + + ws.on('open', () => { + console.log('✅ WebSocket connection established'); + }); + + ws.on('message', (data) => { + const event = JSON.parse(data); + handleWebSocketEvent(event); + }); + + ws.on('close', () => { + console.log('❌ WebSocket connection closed. Reconnecting...'); + setTimeout(connectWebSocket, 5000); + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); +} + +// ============================================ +// STEP 3: Handle WebSocket Events +// ============================================ + +function handleWebSocketEvent(event) { + const { event: eventType, payload } = event; + const meetingId = String(payload?.object?.id); + + // CRITICAL: Filter events - only process tracked meetings + if (!meetingId || !trackedMeetings.has(meetingId)) { + return; + } + + console.log(`📩 WebSocket Event: ${eventType} for meeting ${meetingId}`); + + switch (eventType) { + case 'meeting.started': + console.log(`✅ Meeting started: ${payload.object.topic}`); + break; + + case 'meeting.ended': + console.log(`🏁 Meeting ended: ${payload.object.topic}`); + trackedMeetings.delete(meetingId); + break; + + case 'meeting.participant_joined': + console.log(`👋 ${payload.object.participant.user_name} joined`); + break; + + case 'meeting.participant_left': + console.log(`👋 ${payload.object.participant.user_name} left`); + break; + } +} + +// ============================================ +// USAGE: Track Meeting and Receive Events +// ============================================ + +async function trackMeeting(meetingId) { + // Get meeting details + const meeting = await getMeetingDetails(meetingId); + + // Add to tracked set + trackedMeetings.add(String(meeting.id)); + + console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`); + console.log(` Events will arrive via WebSocket connection`); + + return meeting; +} + +// Initialize WebSocket connection +connectWebSocket(); + +// Track specific meetings +trackMeeting('123456789').catch(console.error); +trackMeeting('987654321').catch(console.error); +``` + +**Comparison with Webhooks:** + +| Aspect | WebSockets | Webhooks | +|--------|------------|----------| +| **Latency** | Lower (persistent connection) | Higher (new HTTP request per event) | +| **Setup** | More complex (S2S OAuth required) | Simpler (just endpoint URL) | +| **Security** | No exposed endpoint | Requires public endpoint + signature verification | +| **Best for** | Real-time dashboards, high-frequency events | Standard backend processing | + +--- + +## Pattern 3: REST API + Meeting SDK Events (Client-Side In-Meeting) + +**When to use:** +- Building custom meeting UI with real-time updates +- In-meeting participant tracking +- User joins meeting from your web app +- Real-time UI state updates (active speaker, video on/off, etc.) + +**Skills needed:** `zoom-rest-api` → `zoom-meeting-sdk` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE (User-initiated): │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting number, password │ +│ └── Generate SDK signature (JWT) │ +│ │ +│ Step 2: ZoomMtg.init() + join() (zoom-meeting-sdk) │ +│ └── User joins meeting in browser │ +│ └── Receive ZoomMtg instance for event listeners │ +│ │ +│ Step 3: ZoomMtg.inMeetingServiceListener() (zoom-meeting-sdk) │ +│ └── Listen to: onUserJoin, onUserLeave, etc. │ +│ └── Events ONLY for this specific meeting │ +│ └── Real-time UI updates │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +**Backend: Generate Signature** + +```javascript +const KJUR = require('jsrsasign'); + +// Step 1: Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return { + id: response.data.id, + password: response.data.password, + topic: response.data.topic, + join_url: response.data.join_url + }; +} + +// Generate Meeting SDK signature +function generateSignature(meetingNumber, role) { + const iat = Math.floor(Date.now() / 1000) - 30; + const exp = iat + 60 * 60 * 2; // 2 hours + + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { + sdkKey: process.env.ZOOM_SDK_KEY, + mn: String(meetingNumber).replace(/\D/g, ''), + role: parseInt(role, 10), // 0 = participant, 1 = host + iat, + exp, + tokenExp: exp + }; + + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify(header), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); +} + +// API endpoint: Get meeting details + signature +app.get('/api/meetings/:meetingId/join', async (req, res) => { + try { + const { meetingId } = req.params; + const { role = 0 } = req.query; + + // Get meeting details from Zoom API + const meeting = await getMeetingDetails(meetingId); + + // Generate signature for SDK + const signature = generateSignature(meeting.id, role); + + res.json({ + meetingNumber: meeting.id, + password: meeting.password, + signature: signature, + sdkKey: process.env.ZOOM_SDK_KEY, + topic: meeting.topic + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + error: error.message + }); + } +}); +``` + +**Frontend: Join Meeting and Listen to Events** + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Step 1: Get meeting details + signature from backend +async function getMeetingCredentials(meetingId) { + const response = await fetch(`/api/meetings/${meetingId}/join`); + return response.json(); +} + +// Step 2: Initialize Meeting SDK +async function joinMeeting(meetingId, userName) { + try { + // Get meeting details from backend (includes REST API call) + const credentials = await getMeetingCredentials(meetingId); + + // Preload SDK + ZoomMtg.preLoadWasm(); + ZoomMtg.prepareWebSDK(); + + // Initialize SDK + ZoomMtg.init({ + leaveUrl: window.location.origin, + patchJsMedia: true, + leaveOnPageUnload: true, + success: () => { + // Join meeting + ZoomMtg.join({ + signature: credentials.signature, + sdkKey: credentials.sdkKey, + meetingNumber: credentials.meetingNumber, + passWord: credentials.password, + userName: userName, + success: () => { + console.log('✅ Joined meeting:', credentials.topic); + + // Step 3: Subscribe to in-meeting events + subscribeToMeetingEvents(); + }, + error: (err) => { + console.error('Join error:', err); + } + }); + }, + error: (err) => { + console.error('Init error:', err); + } + }); + } catch (error) { + console.error('Failed to join meeting:', error); + } +} + +// Step 3: Listen to Meeting SDK events +function subscribeToMeetingEvents() { + // User join events + ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('👋 User joined:', data); + // data.userId, data.userName + // Update UI: add participant to list + }); + + // User leave events + ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('👋 User left:', data); + // Update UI: remove participant from list + }); + + // Active speaker detection + ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + console.log('🎤 Active speaker:', data); + // data: [{ userId, userName }] + // Update UI: highlight speaker + }); + + // Meeting status changes + ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + console.log('📊 Meeting status:', data); + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + }); + + // Chat messages + ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('💬 Chat message:', data); + // Display in custom UI + }); + + // Recording status + ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('🔴 Recording status:', data); + // Show recording indicator + }); + + // User updates (audio/video state changes) + ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('🔄 User updated:', data); + // Update UI: show muted/video off indicators + }); +} + +// Usage +joinMeeting('123456789', 'John Doe'); +``` + +**Key Differences from Webhooks/WebSockets:** + +| Aspect | Meeting SDK Events | Webhooks/WebSockets | +|--------|-------------------|---------------------| +| **Scope** | Meeting-specific (only joined meeting) | Account-level (all meetings) | +| **Location** | Client-side (browser) | Server-side | +| **Authentication** | SDK signature (JWT) | OAuth access token | +| **Use case** | In-meeting UI updates | Backend processing | +| **Event types** | Participant, audio/video, chat, recording | Meeting lifecycle, participants | + +--- + +## Pattern 4: REST API + RTMS (Bot-Based Real-Time Media + Events) + +**When to use:** +- AI transcription, translation, sentiment analysis +- Meeting recording bots +- Real-time media processing (audio/video streams) +- Compliance monitoring + +**Skills needed:** `zoom-rest-api` → `rtms` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE: │ +│ │ +│ 1. Configure webhook subscription for meeting.rtms_started │ +│ 2. Deploy bot infrastructure │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details │ +│ │ +│ Step 2: Wait for meeting.rtms_started webhook │ +│ └── Contains rtmsSessionID, meetingUUID │ +│ │ +│ Step 3: Connect to RTMS WebSocket (rtms) │ +│ └── wss://rtms.zoom.us/ws │ +│ └── Subscribe to: audio, video, transcript, chat │ +│ │ +│ Step 4: Receive real-time media + events │ +│ └── Audio chunks, video frames, transcripts │ +│ └── Events: participant joined/left, speaking status │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +**See comprehensive implementation:** [rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) + +**Quick Overview:** + +```javascript +const axios = require('axios'); +const WebSocket = require('ws'); + +// Step 1: Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Step 2: Wait for meeting.rtms_started webhook +app.post('/webhooks/zoom', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.rtms_started') { + const { rtmsSessionID, meetingUUID } = payload.object; + + // Step 3: Connect to RTMS + connectToRTMS(rtmsSessionID, meetingUUID); + } + + res.status(200).send(); +}); + +// Step 3: Connect to RTMS WebSocket +async function connectToRTMS(sessionID, meetingUUID) { + const token = await getAccessToken(); + + const ws = new WebSocket('wss://rtms.zoom.us/ws', { + headers: { + 'Authorization': `Bearer ${token}`, + 'X-Zoom-Session-ID': sessionID + } + }); + + ws.on('open', () => { + // Subscribe to media types + ws.send(JSON.stringify({ + type: 'subscribe', + mediaTypes: ['audio', 'transcript', 'chat'] + })); + }); + + ws.on('message', (data) => { + const message = JSON.parse(data); + + // Step 4: Handle real-time media and events + switch (message.type) { + case 'audio': + processAudioChunk(message.data); + break; + + case 'transcript': + console.log(`📝 Transcript: ${message.data.text}`); + break; + + case 'chat': + console.log(`💬 Chat: ${message.data.message}`); + break; + + case 'participant_joined': + console.log(`👋 ${message.data.user_name} joined`); + break; + + case 'participant_left': + console.log(`👋 ${message.data.user_name} left`); + break; + } + }); +} + +function processAudioChunk(audioData) { + // Process raw audio for transcription, analysis, etc. +} +``` + +**RTMS Events (Different from Webhooks):** + +| Event Type | Source | Data | +|------------|--------|------| +| `participant_joined` | RTMS WebSocket | Real-time when user joins | +| `participant_left` | RTMS WebSocket | Real-time when user leaves | +| `active_speaker_change` | RTMS WebSocket | Current speaker info | +| `audio` | RTMS WebSocket | Raw audio stream (PCM) | +| `video` | RTMS WebSocket | Raw video frames | +| `transcript` | RTMS WebSocket | Live transcription | + +--- + +## Pattern 5: REST API + Reports (Historical Data, Not Real Events) + +**When to use:** +- Historical analysis, batch processing +- Audit logs, compliance reports +- No real-time requirements +- Backup/recovery systems + +**Skills needed:** `zoom-rest-api` only + +### Implementation + +```javascript +// Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Poll for meeting participants report (after meeting ends) +async function getMeetingParticipantsReport(meetingId) { + const token = await getAccessToken(); + + // Get past meeting details + const response = await axios.get( + `https://api.zoom.us/v2/past_meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Get detailed participant report +async function getParticipantsList(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/report/meetings/${meetingId}/participants`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data.participants; +} + +// Usage: Retrieve meeting + get historical data +async function analyzeMeeting(meetingId) { + // Get meeting metadata + const meeting = await getMeetingDetails(meetingId); + console.log(`Meeting: ${meeting.topic}`); + + // Wait for meeting to end, then get reports + // (typically poll or wait for meeting.ended webhook) + + const participants = await getParticipantsList(meetingId); + console.log(`Total participants: ${participants.length}`); + + participants.forEach(p => { + console.log(`- ${p.name}: ${p.duration} minutes`); + }); +} +``` + +**Not Real-Time Events:** +- Reports API only provides historical data after meeting ends +- Not suitable for real-time monitoring +- No WebSocket/webhook integration +- Use for: auditing, analytics, compliance + +--- + +## Comparison: When to Use Each Pattern + +| Pattern | Latency | Scope | Setup Complexity | Best For | +|---------|---------|-------|------------------|----------| +| **Webhooks** | Medium | Account-level | Low | Standard backend processing | +| **WebSockets** | Low | Account-level | Medium | Real-time dashboards, no exposed endpoint | +| **Meeting SDK** | Very Low | Meeting-specific | Low | In-meeting UI, participant views | +| **RTMS** | Very Low | Meeting-specific | High | AI transcription, recording bots | +| **Reports API** | N/A (historical) | Account-level | Low | Batch processing, analytics | + +--- + +## Skill Chaining Summary + +| Scenario | Skills Chain | Order | +|----------|--------------|-------| +| Backend event processing | `zoom-rest-api` → `webhooks` | 1. Get meeting, 2. Filter webhook events | +| Low-latency monitoring | `zoom-rest-api` → `zoom-websockets` | 1. Get meeting, 2. Filter WebSocket events | +| Custom meeting UI | `zoom-rest-api` → `zoom-meeting-sdk` | 1. Get details, 2. Join meeting, 3. Listen to SDK events | +| AI transcription bot | `zoom-rest-api` → `rtms` | 1. Get meeting, 2. Wait for webhook, 3. Connect to RTMS | +| Historical analysis | `zoom-rest-api` only | 1. Get meeting, 2. Poll reports after meeting ends | + +--- + +## Authorization Requirements + +**All patterns require different OAuth scopes:** + +| Pattern | Required Scopes | +|---------|----------------| +| REST API | `meeting:read:admin` or `meeting:read` | +| Webhooks | Event subscription in app config | +| WebSockets | Server-to-Server OAuth + event subscription | +| Meeting SDK | SDK Key/Secret + signature generation | +| RTMS | `meeting:read:admin` + RTMS-enabled account | + +**See also**: [Authorization Patterns](../references/authorization-patterns.md) for complete scope validation strategies. + +--- + +## Common Pitfalls + +### ❌ Misconception: "Subscribe to Events for a Specific Meeting" + +**Reality**: Most Zoom event systems are **account-level**, not meeting-specific. + +| What You Think | What Actually Happens | +|----------------|----------------------| +| "Subscribe to events for meeting 123456789" | You receive events for ALL meetings in the account | +| "Dynamically enable webhooks when meeting starts" | Webhooks are configured at app creation time | +| "Create a webhook subscription per meeting" | One subscription receives events for all meetings | + +**Solution**: Filter events by meeting ID in your handler code. + +### ❌ Misconception: "All Events Are the Same" + +**Reality**: Different event systems provide different event types: + +| Event System | Events | +|--------------|--------| +| **Webhooks** | Meeting lifecycle: started, ended, participant_joined/left, recording_completed | +| **WebSockets** | Same as webhooks, but lower latency | +| **Meeting SDK** | In-meeting: onUserJoin, onActiveSpeaker, onUserUpdate, onReceiveChatMsg | +| **RTMS** | Real-time media: participant events + audio/video streams + transcripts | + +**Solution**: Choose the right event system for your use case (see comparison table above). + +--- + +## Related Use Cases + +- **[Meeting Details with Events](meeting-details-with-events.md)** - Expanded webhook implementation +- **[Meeting Bots](meeting-bots.md)** - Building RTMS bots for transcription +- **[Recording & Transcription](recording-transcription.md)** - Download recordings after meeting ends + +--- + +## Resources + +- **REST API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Webhooks**: https://developers.zoom.us/docs/api/rest/webhook-reference/ +- **WebSockets**: https://developers.zoom.us/docs/api/websockets/ +- **Meeting SDK**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **RTMS**: https://developers.zoom.us/docs/rtms/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/rivet-event-driven-api-orchestrator.md b/partner-built/zoom-plugin/skills/general/use-cases/rivet-event-driven-api-orchestrator.md new file mode 100644 index 00000000..2f3d597a --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/rivet-event-driven-api-orchestrator.md @@ -0,0 +1,43 @@ +# Rivet Event-Driven API Orchestrator + +Use Rivet to build a Node.js backend that combines webhook event handling with Zoom REST API actions using typed module clients. + +## When to Use + +- You need both event-driven workflows and API calls in one service. +- You want to reduce custom OAuth/webhook boilerplate. +- You are composing multiple Zoom surfaces (Team Chat, Meetings, Users, Phone, Video SDK API). + +## Skill Chain + +- [rivet-sdk](../../rivet-sdk/SKILL.md) +- [oauth](../../oauth/SKILL.md) +- [rest-api](../../rest-api/SKILL.md) +- [team-chat](../../team-chat/SKILL.md) + +## Architecture + +```text +Zoom Events -> Rivet webEventConsumer -> business logic -> Rivet endpoints.* -> Zoom APIs +``` + +## High-Level Flow + +1. Instantiate one or more Rivet module clients. +2. Register webhook event handlers. +3. Start receiver(s) and expose `/zoom/events` endpoint(s). +4. Call typed endpoint wrappers from event handlers. +5. Persist state/tokens and return user-visible results. + +## Key Risks + +- Incorrect per-module endpoint port mapping. +- OAuth redirect/state mismatch. +- Scope mismatch for endpoint calls. +- Event payload drift across versions. + +## See Also + +- [rivet-sdk examples](../../rivet-sdk/examples/getting-started-pattern.md) +- [rivet-sdk multi-client pattern](../../rivet-sdk/examples/multi-client-pattern.md) +- [rivet-sdk runbook](../../rivet-sdk/RUNBOOK.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/saas-app-oauth-integration.md b/partner-built/zoom-plugin/skills/general/use-cases/saas-app-oauth-integration.md new file mode 100644 index 00000000..e88e87ca --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/saas-app-oauth-integration.md @@ -0,0 +1,196 @@ +# Building a SaaS App with Zoom OAuth Integration + +Create a multi-tenant SaaS application that integrates with Zoom using User OAuth for per-user authorization. + +## Scenario + +You're building a meeting scheduling SaaS that needs to: +- Allow users to authorize your app to access their Zoom account +- Create meetings on behalf of users +- List user's meetings +- Store and refresh tokens securely per user +- Handle token expiration and refresh automatically + +## Required Skills + +1. **oauth** - User authorization flow, token management +2. **zoom-rest-api** - Meeting creation and management endpoints + +## Architecture + +``` +User Browser → Your SaaS App → Zoom OAuth → Zoom APIs + ↓ + Database (user tokens) +``` + +## Implementation Steps + +### 1. OAuth Setup (oauth) + +**Configure app in Zoom Marketplace:** +- App Type: OAuth +- Redirect URL: `https://yourapp.com/oauth/callback` +- Required scopes: `meeting:write`, `meeting:read`, `user:read` + +**See:** `oauth/concepts/oauth-flows.md#user-authorization-oauth` + +### 2. User Authorization Flow (oauth) + +```javascript +// Redirect user to Zoom authorization +app.get('/connect-zoom', (req, res) => { + const state = generateSecureState(); + req.session.oauthState = state; + + res.redirect( + `https://zoom.us/oauth/authorize?` + + `response_type=code&` + + `client_id=${CLIENT_ID}&` + + `redirect_uri=${REDIRECT_URI}&` + + `state=${state}` + ); +}); +``` + +**See:** `oauth/examples/user-oauth-mysql.md` + +### 3. Token Storage (oauth) + +Store user tokens with encryption: + +```javascript +// After OAuth callback +const { access_token, refresh_token } = await exchangeCode(code); + +await db.users.update({ + zoom_access_token: encrypt(access_token), + zoom_refresh_token: encrypt(refresh_token), + token_expiry: Date.now() + 3600000 +}, { where: { id: userId } }); +``` + +**See:** `oauth/concepts/token-lifecycle.md#user-oauth--device-flow-token-lifecycle` + +### 4. Auto-Refresh Middleware (oauth) + +```javascript +// Automatically refresh expired tokens +const zoomTokenMiddleware = async (req, res, next) => { + const user = await db.users.findByPk(req.session.userId); + + if (isTokenExpired(user.token_expiry)) { + const newTokens = await refreshToken(user.zoom_refresh_token); + await updateUserTokens(user.id, newTokens); + req.zoomToken = newTokens.access_token; + } else { + req.zoomToken = decrypt(user.zoom_access_token); + } + + next(); +}; +``` + +**See:** `oauth/examples/token-refresh.md` + +### 5. Create Meetings (zoom-rest-api) + +```javascript +app.post('/api/meetings', zoomTokenMiddleware, async (req, res) => { + const meeting = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: req.body.topic, + type: 2, // Scheduled meeting + start_time: req.body.start_time, + duration: req.body.duration + }, + { + headers: { Authorization: `Bearer ${req.zoomToken}` } + } + ); + + res.json(meeting.data); +}); +``` + +**See:** `zoom-rest-api` skill for endpoint documentation + +## Security Considerations + +### Token Encryption (oauth) + +**MUST encrypt tokens at rest:** +```javascript +const crypto = require('crypto'); + +function encrypt(text) { + const cipher = crypto.createCipher('aes-256-cbc', process.env.CIPHER_KEY); + return cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); +} +``` + +**See:** `oauth/examples/user-oauth-mysql.md#token-encryption` + +### State Parameter (oauth) + +**Prevent CSRF attacks:** +```javascript +const state = crypto.randomBytes(16).toString('hex'); +req.session.oauthState = state; // Verify in callback +``` + +**See:** `oauth/concepts/state-parameter.md` + +## Handling Edge Cases + +### User Revokes Access (webhooks) + +Listen for deauthorization webhook: + +```javascript +app.post('/webhooks/zoom', (req, res) => { + if (req.body.event === 'app_deauthorized') { + const userId = req.body.payload.user_id; + await deleteUserZoomTokens(userId); + } +}); +``` + +**Chain to:** `webhooks` skill for webhook setup + +### Refresh Token Expired (oauth) + +```javascript +try { + await refreshToken(user.zoom_refresh_token); +} catch (error) { + if (error.code === 4735) { + // Refresh token expired - prompt re-authorization + res.redirect('/connect-zoom'); + } +} +``` + +**See:** `oauth/troubleshooting/token-issues.md` + +## Testing Checklist + +- [ ] User authorization flow works +- [ ] Tokens stored encrypted in database +- [ ] Tokens auto-refresh before expiration +- [ ] Meetings created successfully via API +- [ ] Deauthorization webhook handled +- [ ] Refresh token expiration handled + +## Related Use Cases + +- `meeting-automation.md` - Advanced meeting scheduling +- `user-and-meeting-creation.md` - Bulk user/meeting operations +- `recording-download-pipeline.md` - Download recordings via API + +## Skills Used + +- **oauth** (primary) - User OAuth, token management, PKCE +- **zoom-rest-api** - Meeting and user API endpoints +- **webhooks** - Deauthorization notifications diff --git a/partner-built/zoom-plugin/skills/general/use-cases/scribe-transcription-pipeline.md b/partner-built/zoom-plugin/skills/general/use-cases/scribe-transcription-pipeline.md new file mode 100644 index 00000000..d9007a07 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/scribe-transcription-pipeline.md @@ -0,0 +1,58 @@ +# Scribe Transcription Pipeline + +Use AI Services Scribe when the input is already a file or storage object and the output should be a transcript JSON payload or transcript files. + +## Skill Chain + +- Primary: [../../scribe/SKILL.md](../../scribe/SKILL.md) +- Optional storage/download source: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Optional webhook hardening: [../../webhooks/SKILL.md](../../webhooks/SKILL.md) + +## When to Use Scribe + +Use `scribe` for: +- one uploaded file that should be transcribed immediately +- S3 archive transcription in the background +- post-processing exported media files into searchable transcript data + +Do not use `scribe` for: +- live in-meeting media stream ingestion +- bot-style participant join and raw recording + +For those, use: +- [../../rtms/SKILL.md](../../rtms/SKILL.md) for live media streams +- [../../meeting-sdk/linux/SKILL.md](../../meeting-sdk/linux/SKILL.md) for visible meeting bots + +## Minimal Flow + +```text +input file or storage prefix + -> generate Build JWT + -> choose fast mode or batch mode + -> submit Scribe request + -> receive transcript JSON or batch job state + -> persist transcript output +``` + +## Typical Variants + +1. Fast mode + - one short file + - immediate response needed + - `POST /aiservices/scribe/transcribe` + +2. Batch mode + - long recordings or many files + - `POST /aiservices/scribe/jobs` + - monitor with polling or webhook notifications + +3. Zoom recording re-transcription + - use REST API to download or export recording files + - feed those files into Scribe for your own transcript settings + +## Common Failure Points + +- wrong credential type (Build JWT vs normal OAuth token) +- choosing RTMS for offline archive transcription +- expired S3 credentials for batch jobs +- webhook signature verification implemented after JSON parsing instead of on raw body diff --git a/partner-built/zoom-plugin/skills/general/use-cases/sdk-size-optimization.md b/partner-built/zoom-plugin/skills/general/use-cases/sdk-size-optimization.md new file mode 100644 index 00000000..c7bcf700 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/sdk-size-optimization.md @@ -0,0 +1,195 @@ +# SDK Size Optimization + +Reduce mobile app binary size when integrating Zoom Meeting SDK or Video SDK on iOS and Android. + +## Overview + +Zoom SDKs add significant size to mobile apps. This guide covers **Zoom-recommended techniques** from the official developer forum and blog to minimize the impact on your app's download size. + +## Skills Needed + +- **zoom-meeting-sdk** (iOS, Android) +- **zoom-video-sdk** (iOS, Android) + +## SDK Size Reference + +### Meeting SDK + +| Platform | Configuration | Size | +|----------|---------------|------| +| iOS | Universal (all architectures) | ~107 MB | +| Android | arm64-v8a + armeabi-v7a | ~97-108 MB | +| Android | arm64-v8a only | ~71 MB | +| Android | armeabi-v7a only | ~47 MB | + +### Video SDK + +| Platform | Size | +|----------|------| +| iOS/Android | ~75 MB | + +--- + +## Android: Zoom-Recommended Methods + +### 1. ABI Filtering (Official Recommendation) + +This is the **primary method recommended by Zoom** to reduce APK size. Filter to only the CPU architectures you need. + +```gradle +// build.gradle (app module) +android { + defaultConfig { + ndk { + // Option 1: Modern devices only (smallest size) + abiFilters 'arm64-v8a' + + // Option 2: Broader device support + // abiFilters 'arm64-v8a', 'armeabi-v7a' + } + } +} +``` + +**Size Impact (from Zoom Dev Forum):** + +| Configuration | APK Size | +|---------------|----------| +| No Zoom SDK | ~11 MB | +| arm64-v8a + armeabi-v7a | ~97 MB | +| **arm64-v8a only** | **~71 MB** | +| armeabi-v7a only | ~47 MB | + +**Note:** Most modern Android devices (2017+) use `arm64-v8a`. Only include `armeabi-v7a` if you need to support older 32-bit devices. + +### 2. Android App Bundle (AAB) + +Use Android App Bundles for Play Store distribution. Google Play automatically generates device-specific APKs: + +```gradle +android { + bundle { + abi { + enableSplit = true + } + } +} +``` + +Users only download the architecture matching their device, reducing download size. + +--- + +## iOS: Zoom-Recommended Methods + +### App Store App Thinning (Automatic) + +iOS App Store automatically applies App Thinning: + +- Delivers only the architecture slice needed for each device +- No manual configuration required +- Happens automatically when distributing through App Store + +**Verify in Xcode:** +Window → Organizer → Archives → App Thinning Report + +--- + +## What Does NOT Work + +Zoom has confirmed the following approaches are **NOT supported**: + +### No Feature/Module Exclusion + +From Zoom Developer Forum (August 2024): +> "There are **no new updates regarding reducing size of SDK**" + +- Cannot remove virtual background module +- Cannot remove screen sharing module +- Cannot exclude any bundled features +- All features are compiled together + +### No Dynamic Feature Module Support + +Zoom has confirmed they do **NOT support** Android Dynamic Feature Modules: + +- Resource linking errors occur +- `Resources$NotFoundException` at runtime +- Community workarounds are unsupported and may break + +### ProGuard/R8 Not Fully Supported + +**Warning:** ProGuard/R8 causes crashes with Zoom SDK, even with Zoom's provided rules. Users report runtime crashes. Use at your own risk. + +### Bitcode Not Supported (iOS) + +Zoom SDK does **NOT** support iOS Bitcode due to internal dependencies. However, Bitcode is no longer required by Apple (Xcode 15+). + +--- + +## Alternative Approaches + +If SDK size is prohibitive for your use case: + +### 1. Web SDK in WebView + +Load the Web SDK in a native WebView instead of using the native SDK: + +```swift +// iOS +let webView = WKWebView(frame: view.bounds) +webView.load(URLRequest(url: URL(string: "https://your-app.com/zoom-meeting")!)) +``` + +```kotlin +// Android +webView.loadUrl("https://your-app.com/zoom-meeting") +``` + +**Trade-offs:** +- No native SDK size impact +- Reduced native feature access +- Requires WebView setup and web hosting + +### 2. Deep Link to Zoom App + +Open meetings in the native Zoom app instead of embedding: + +```swift +// iOS +if let url = URL(string: "zoomus://zoom.us/join?confno=\(meetingNumber)") { + UIApplication.shared.open(url) +} +``` + +```kotlin +// Android +val intent = Intent(Intent.ACTION_VIEW, + Uri.parse("zoomus://zoom.us/join?confno=$meetingNumber")) +startActivity(intent) +``` + +**Trade-offs:** +- Zero SDK size impact +- Requires Zoom app to be installed +- Less integrated user experience + +--- + +## Summary + +| Platform | Recommended Method | Expected Savings | +|----------|-------------------|------------------| +| **Android** | `abiFilters 'arm64-v8a'` | ~26 MB | +| **Android** | App Bundle (AAB) | Automatic per-device | +| **iOS** | App Thinning (automatic) | Automatic per-device | + +**Key Limitation:** Zoom does not currently offer modular SDK downloads or feature exclusion. The SDK includes all features bundled together. + +--- + +## Resources + +- **Zoom Blog - Reduce APK Size**: https://developers.zoom.us/blog/reduce-apk-size-zoom-meeting-sdk-android/ +- **Zoom Developer Forum - SDK Size Discussion**: https://devforum.zoom.us/t/reducing-the-size-of-the-zoom-meeting-sdk-ios-android/94302 +- **Android ABI Management**: https://developer.android.com/ndk/guides/abis diff --git a/partner-built/zoom-plugin/skills/general/use-cases/sdk-wrappers-gui.md b/partner-built/zoom-plugin/skills/general/use-cases/sdk-wrappers-gui.md new file mode 100644 index 00000000..b4fedb6c --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/sdk-wrappers-gui.md @@ -0,0 +1,560 @@ +# SDK Wrappers and GUI Integration + +Building custom language wrappers and GUI applications with Zoom Meeting/Video SDKs. + +## Overview + +The native Zoom SDKs are written in C++. To use them from other languages or with GUI frameworks, you need wrappers or direct integration. + +| Platform | Wrapper/Integration | Use Case | +|----------|---------------------|----------| +| Windows | C++/CLI → C# | WPF, WinForms, .NET apps | +| Linux | Native C++ | Qt, GTK desktop apps | +| Linux | Direct C++ | Headless bots, server-side | + +## Windows: C++/CLI Wrapper for C#/.NET + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ C# Application Layer (WPF / WinForms / .NET) │ +│ - UI controls, business logic │ +│ - Uses managed ZOOM_SDK_DOTNET_WRAP namespace │ +└─────────────────────────────────────────────────────────────┘ + ↕ Managed/Unmanaged Boundary +┌─────────────────────────────────────────────────────────────┐ +│ C++/CLI Wrapper Layer (zoom_sdk_dotnet_wrap.dll) │ +│ - Managed ref classes with ^ handles │ +│ - Native C++ classes calling SDK │ +│ - Event bridging: std::bind → C# delegates │ +└─────────────────────────────────────────────────────────────┘ + ↕ Native C++ Interface +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (videosdk.dll / sdk.dll) │ +│ - Loaded dynamically via sdk_dll_path │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Why C++/CLI (Not P/Invoke)? + +| Aspect | P/Invoke | C++/CLI | +|--------|----------|---------| +| Complex C++ objects | ❌ Difficult | ✅ Native support | +| Callbacks/delegates | ❌ Manual marshaling | ✅ Automatic bridging | +| Object lifetime | ❌ Manual GC pinning | ✅ Automatic handling | +| std::function patterns | ❌ Not supported | ✅ Works with std::bind | + +### Project Structure + +``` +videosdk-windows-dotnet-quickstart/ +├── ZoomVideoSDK.CSharp.sln # Visual Studio solution +├── config.json # Runtime configuration +├── sdk/ # Zoom SDK files +│ └── x64/ +│ ├── h/ # C++ headers +│ ├── lib/ # Static libraries (.lib) +│ └── bin/ # Runtime DLLs (.dll) +├── ZoomVideoSDK.Wrapper/ # C++/CLI wrapper project +│ ├── ZoomVideoSDK.Wrapper.vcxproj # CLRSupport=true +│ ├── ZoomSDKManager.h # Managed wrapper class +│ └── ZoomSDKManager.cpp +├── ZoomVideoSDK.WinForms/ # C# WinForms app +│ ├── MainForm.cs +│ └── ZoomSDKInterop.cs +└── ZoomVideoSDK.WPF/ # C# WPF app + ├── MainWindow.xaml + └── MainWindow.xaml.cs +``` + +### C++/CLI Wrapper Implementation + +**Project Configuration (.vcxproj)**: +```xml + + DynamicLibrary + true + Unicode + +``` + +**Wrapper Class (C++/CLI)**: +```cpp +// ZoomSDKManager.h +#pragma once + +using namespace System; + +namespace ZoomVideoSDK { + namespace Wrapper { + + // Managed delegate (callable from C#) + public delegate void SessionStatusChangedHandler(String^ status, String^ message); + + // Managed wrapper class + public ref class ZoomSDKManager sealed { + public: + static property ZoomSDKManager^ Instance { + ZoomSDKManager^ get() { return m_Instance; } + } + + bool Initialize(String^ sdkDllPath); + bool JoinSession(String^ sessionName, String^ token, String^ userName); + void LeaveSession(); + + // Events exposed to C# + event SessionStatusChangedHandler^ SessionStatusChanged; + + private: + static ZoomSDKManager^ m_Instance = gcnew ZoomSDKManager; + + // Bridge to invoke C# events from native callbacks + void OnSessionStatusChanged(String^ status, String^ message) { + SessionStatusChanged(status, message); + } + }; + } +} +``` + +**Callback Bridging Pattern**: +```cpp +// Native C++ handler class (unmanaged) +class NativeEventHandler { +public: + static NativeEventHandler& GetInst() { + static NativeEventHandler inst; + return inst; + } + + void onSessionJoin() { + // Forward to managed wrapper using QMetaObject pattern + ZoomSDKManager::Instance->OnSessionStatusChanged("Joined", "Session joined successfully"); + } +}; + +// Binding native callbacks to handler +void ZoomSDKManager::BindEvents() { + // Use std::bind to connect native callbacks + ZOOM_SDK::GetSessionService().m_cbonSessionJoin = + std::bind(&NativeEventHandler::onSessionJoin, &NativeEventHandler::GetInst()); +} +``` + +### C# Application Usage + +**WPF Example**: +```csharp +using ZoomVideoSDK.Wrapper; + +public partial class MainWindow : Window +{ + private ZoomSDKManager _sdk; + + public MainWindow() + { + InitializeComponent(); + + _sdk = ZoomSDKManager.Instance; + _sdk.SessionStatusChanged += OnSessionStatusChanged; + } + + private void JoinButton_Click(object sender, RoutedEventArgs e) + { + bool initialized = _sdk.Initialize(@".\sdk\x64\bin"); + if (initialized) + { + _sdk.JoinSession( + sessionName: SessionNameTextBox.Text, + token: JwtTokenTextBox.Text, + userName: UserNameTextBox.Text + ); + } + } + + private void OnSessionStatusChanged(string status, string message) + { + // Must invoke on UI thread + Dispatcher.Invoke(() => { + StatusLabel.Content = $"{status}: {message}"; + }); + } +} +``` + +**WinForms Example**: +```csharp +public partial class MainForm : Form +{ + private ZoomSDKManager _sdk; + + private void OnSessionStatusChanged(string status, string message) + { + // Must invoke on UI thread + this.Invoke((MethodInvoker)delegate { + statusLabel.Text = $"{status}: {message}"; + }); + } +} +``` + +### Build Configuration + +**DLL Dependencies**: +``` +C# App (managed) + ↓ references +ZoomVideoSDK.Wrapper.dll (C++/CLI mixed-mode) + ↓ loads dynamically at runtime +videosdk.dll + dependencies (native) +``` + +**Post-Build Events**: +```batch +REM Copy SDK DLLs to output directory +xcopy /Y "$(SolutionDir)sdk\x64\bin\*.dll" "$(OutDir)" +``` + +--- + +## Linux: Qt GUI Integration + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Qt Application (C++) │ +│ - Qt Widgets / QML │ +│ - Signals/Slots for event handling │ +└─────────────────────────────────────────────────────────────┘ + ↕ Direct C++ calls +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (libvideosdk.so) │ +│ - Video/Audio processing │ +│ - Network communication │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Project Structure + +``` +videosdk-linux-qt-quickstart/ +├── README.md +├── run_qt_demo.sh # Run script +├── src/ +│ ├── CMakeLists.txt # Qt6/Qt5 build config +│ ├── config.json # Session config +│ ├── bin/ # Build output +│ │ └── VideoSDKQtDemo +│ ├── include/ # SDK headers +│ ├── lib/ # SDK libraries +│ └── Source Files: +│ ├── zoom_v-sdk_linux_bot_qt.cpp # Main entry +│ ├── QtMainWindow.h/cpp # Main window +│ ├── QtVideoWidget.h/cpp # Video display +│ ├── QtVideoRenderer.h/cpp # YUV→RGB rendering +│ ├── QtPreviewVideoHandler.h/cpp # Self video +│ └── QtRemoteVideoHandler.h/cpp # Remote video +``` + +### Qt Video Rendering + +```cpp +// QtVideoWidget.h +class QtVideoWidget : public QWidget { + Q_OBJECT + +public: + explicit QtVideoWidget(QWidget* parent = nullptr); + void updateFrame(const QImage& frame); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QImage m_currentFrame; + QMutex m_mutex; +}; + +// QtVideoWidget.cpp +void QtVideoWidget::updateFrame(const QImage& frame) { + QMutexLocker lock(&m_mutex); + m_currentFrame = frame; + update(); // Trigger repaint +} + +void QtVideoWidget::paintEvent(QPaintEvent* event) { + QPainter painter(this); + QMutexLocker lock(&m_mutex); + + if (!m_currentFrame.isNull()) { + // Scale to fit with aspect ratio + QImage scaled = m_currentFrame.scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Center the image + int x = (width() - scaled.width()) / 2; + int y = (height() - scaled.height()) / 2; + painter.drawImage(x, y, scaled); + } +} +``` + +### YUV to RGB Conversion + +```cpp +// QtVideoRenderer.cpp +QImage convertYUV420ToRGB(const unsigned char* yuvData, int width, int height) { + QImage image(width, height, QImage::Format_RGB888); + + const unsigned char* yPlane = yuvData; + const unsigned char* uPlane = yuvData + width * height; + const unsigned char* vPlane = uPlane + (width * height / 4); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int yIndex = y * width + x; + int uvIndex = (y / 2) * (width / 2) + (x / 2); + + int Y = yPlane[yIndex]; + int U = uPlane[uvIndex] - 128; + int V = vPlane[uvIndex] - 128; + + // ITU-R BT.601 conversion + int R = qBound(0, (int)(Y + 1.402 * V), 255); + int G = qBound(0, (int)(Y - 0.344 * U - 0.714 * V), 255); + int B = qBound(0, (int)(Y + 1.772 * U), 255); + + image.setPixel(x, y, qRgb(R, G, B)); + } + } + + return image; +} +``` + +### Qt Event Threading + +```cpp +// Invoke SDK callbacks on Qt main thread +class VideoCallback : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + QImage frame = convertYUV420ToRGB(data->getBuffer(), + data->getWidth(), + data->getHeight()); + + // Thread-safe UI update using Qt's event system + QMetaObject::invokeMethod(m_videoWidget, [this, frame]() { + m_videoWidget->updateFrame(frame); + }, Qt::QueuedConnection); + } + +private: + QtVideoWidget* m_videoWidget; +}; +``` + +### Build with CMake + +```cmake +cmake_minimum_required(VERSION 3.14) +project(VideoSDKQtDemo) + +set(CMAKE_CXX_STANDARD 17) + +# Find Qt +find_package(Qt6 COMPONENTS Core Widgets REQUIRED) +# Or: find_package(Qt5 COMPONENTS Core Widgets REQUIRED) + +# Find ALSA for audio +find_package(ALSA REQUIRED) + +# Source files +set(SOURCES + zoom_v-sdk_linux_bot_qt.cpp + QtMainWindow.cpp + QtVideoWidget.cpp + QtVideoRenderer.cpp +) + +add_executable(VideoSDKQtDemo ${SOURCES}) + +target_link_libraries(VideoSDKQtDemo + Qt6::Core Qt6::Widgets + ${ALSA_LIBRARIES} + ${CMAKE_SOURCE_DIR}/lib/libvideosdk.so +) +``` + +--- + +## Linux: GTK GUI Integration + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ GTKmm Application (C++) │ +│ - GTK Widgets (Gtk::Window, Gtk::Box, etc.) │ +│ - Glib signals for event handling │ +│ - SDL2 for video rendering │ +└─────────────────────────────────────────────────────────────┘ + ↕ Direct C++ calls +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (libvideosdk.so) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Project Structure + +``` +videosdk-linux-gtk-quickstart/ +├── README.md +├── src/ +│ ├── CMakeLists.txt +│ ├── config.json +│ ├── bin/ +│ │ └── SkeletonDemo +│ └── Source Files: +│ ├── zoom_v-sdk_linux_bot.cpp # Main entry + GTK UI +│ ├── VideoRenderer.h/cpp # SDL2 video rendering +│ ├── VideoDisplayBridge.h/cpp # SDK→Renderer bridge +│ ├── PreviewVideoHandler.h/cpp # Self video +│ └── RemoteVideoRawDataHandler.h/cpp # Remote video +``` + +### GTK Video Rendering (SDL2) + +```cpp +// VideoRenderer.h +class VideoRenderer { +public: + VideoRenderer(Gtk::Widget* container); + ~VideoRenderer(); + + void renderFrame(const unsigned char* yuvData, int width, int height); + +private: + SDL_Window* m_window; + SDL_Renderer* m_renderer; + SDL_Texture* m_texture; +}; + +// VideoRenderer.cpp +void VideoRenderer::renderFrame(const unsigned char* yuvData, int width, int height) { + // Update YUV texture directly (SDL handles conversion) + SDL_UpdateYUVTexture(m_texture, nullptr, + yuvData, // Y plane + width, // Y pitch + yuvData + width * height, // U plane + width / 2, // U pitch + yuvData + width * height * 5/4, // V plane + width / 2); // V pitch + + SDL_RenderClear(m_renderer); + SDL_RenderCopy(m_renderer, m_texture, nullptr, nullptr); + SDL_RenderPresent(m_renderer); +} +``` + +### GTK Main Window + +```cpp +// zoom_v-sdk_linux_bot.cpp +class MainWindow : public Gtk::Window { +public: + MainWindow() { + set_title("Zoom Video SDK Demo"); + set_default_size(1200, 800); + + // Create layout + auto mainBox = Gtk::make_managed(Gtk::ORIENTATION_VERTICAL); + + // Device selection + m_cameraCombo = Gtk::make_managed(); + m_micCombo = Gtk::make_managed(); + m_speakerCombo = Gtk::make_managed(); + + // Control buttons + m_joinButton = Gtk::make_managed("Join Session"); + m_leaveButton = Gtk::make_managed("Leave Session"); + + // Video areas (SDL embedded) + m_selfVideoArea = Gtk::make_managed(); + m_remoteVideoArea = Gtk::make_managed(); + + // Connect signals + m_joinButton->signal_clicked().connect( + sigc::mem_fun(*this, &MainWindow::on_join_clicked)); + + add(*mainBox); + show_all_children(); + } + +private: + void on_join_clicked() { + // Initialize SDK and join session + ZoomVideoSDKInitParams params; + params.domain = "https://zoom.us"; + m_sdk->initialize(params); + + ZoomVideoSDKSessionContext context; + context.sessionName = m_sessionEntry->get_text().c_str(); + context.token = m_tokenEntry->get_text().c_str(); + m_sdk->joinSession(context); + } +}; +``` + +### Build Dependencies + +```bash +# Ubuntu/Debian +sudo apt install build-essential cmake +sudo apt install libgtkmm-3.0-dev libsdl2-dev +sudo apt install libasound2-dev libcurl4-openssl-dev +``` + +```cmake +# CMakeLists.txt +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTKMM REQUIRED gtkmm-3.0) +pkg_check_modules(SDL2 REQUIRED sdl2) +pkg_check_modules(ALSA REQUIRED alsa) + +target_link_libraries(SkeletonDemo + ${GTKMM_LIBRARIES} + ${SDL2_LIBRARIES} + ${ALSA_LIBRARIES} + ${CMAKE_SOURCE_DIR}/lib/libvideosdk.so +) +``` + +--- + +## Comparison: Qt vs GTK + +| Aspect | Qt | GTK | +|--------|----|----| +| Language | C++ (native) | C++ (GTKmm wrapper) | +| Video Rendering | QPainter / QImage | SDL2 / Cairo | +| Threading | Signals/Slots + QMetaObject | Glib main loop + idle callbacks | +| Build System | CMake / qmake | CMake | +| Look & Feel | Platform-native | GTK theme | +| Learning Curve | Moderate | Moderate | + +## Resources + +### Sample Repositories + +- **Windows C#**: [videosdk-windows-dotnet-desktop-framework-quickstart](https://github.com/tanchunsiong/videosdk-windows-dotnet-desktop-framework-quickstart) +- **Linux Qt**: [videosdk-linux-qt-quickstart](https://github.com/tanchunsiong/videosdk-linux-qt-quickstart) +- **Linux GTK**: [videosdk-linux-gtk-quickstart](https://github.com/tanchunsiong/videosdk-linux-gtk-quickstart) + +### Official Documentation + +- [Windows Meeting SDK](https://developers.zoom.us/docs/meeting-sdk/windows/) +- [Windows Video SDK](https://developers.zoom.us/docs/video-sdk/windows/) +- [Linux Video SDK](https://developers.zoom.us/docs/video-sdk/linux/) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/server-to-server-oauth-with-webhooks.md b/partner-built/zoom-plugin/skills/general/use-cases/server-to-server-oauth-with-webhooks.md new file mode 100644 index 00000000..8ad781d9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/server-to-server-oauth-with-webhooks.md @@ -0,0 +1,43 @@ +# Server-to-Server OAuth With Webhooks (Common Enterprise Pattern) + +This pattern shows up constantly in high-frequency forum clusters: + +- "Can I use Server-to-Server OAuth with webhooks?" +- "How do I validate webhook requests?" +- "How do I automate meeting/user/report operations across the account?" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-rest-api** | Make server-side API calls using S2S tokens | +| 2 | **zoom-oauth** | Correctly mint S2S access tokens (`account_credentials`) | +| 3 | **zoom-webhooks** | Receive events, handle URL validation, verify signatures | + +## Architecture + +1. Your backend periodically requests an S2S access token. +2. Your backend calls REST API endpoints to create/update resources. +3. Zoom calls your webhook endpoint for events (meeting/webinar/recording/etc). +4. Your webhook handler verifies authenticity and enqueues async work. + +## Key Clarifications + +- **Webhooks do not "use" your S2S token**. Webhooks are pushed to your endpoint and verified via webhook secrets/signatures. +- REST API calls and webhook ingestion are separate authentication planes. + +## Hard Requirements + +- Public HTTPS webhook endpoint +- Handle `endpoint.url_validation` +- Verify request signatures (and/or follow Zoom verification guidance) +- Respond `200` quickly; do heavy processing asynchronously + +## Links + +- `../../rest-api/concepts/authentication-flows.md` +- `../../rest-api/examples/webhook-server.md` +- `../../webhooks/references/verification.md` +- `../references/automatic-skill-chaining-rest-webhooks.md` +- `../references/meeting-webhooks-oauth-refresh-orchestration.md` +- `../references/distributed-meeting-fallback-architecture.md` diff --git a/partner-built/zoom-plugin/skills/general/use-cases/team-chat-llm-bot.md b/partner-built/zoom-plugin/skills/general/use-cases/team-chat-llm-bot.md new file mode 100644 index 00000000..00bf5e30 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/team-chat-llm-bot.md @@ -0,0 +1,265 @@ +# Use Case: AI-Powered Team Chat Bot with LLM Integration + +Build an intelligent Zoom Team Chat bot that uses Claude/GPT for natural language understanding and can be chained with other Zoom skills. + +## Scenario + +Create a chatbot that: +1. Responds to natural language queries +2. Can trigger Zoom Meeting creation +3. Can search and retrieve chat history +4. Provides intelligent assistance across Zoom products + +## Skills Required + +- **zoom-team-chat** - Primary skill for chatbot functionality +- **zoom-rest-api** - For meeting creation, user management +- **oauth** - For user authentication flows (optional) +- **zoom-meeting-sdk** - For advanced meeting integrations (optional) + +## Architecture + +``` +User sends message → Team Chat Bot receives webhook + ↓ + Call LLM (Claude/GPT) + ↓ + Parse LLM response for intent + ↓ + ┌──────────────────┼──────────────────┐ + │ │ │ + Create Meeting Get User Info Send Response + (REST API) (REST API) (Team Chat) +``` + +## Implementation Steps + +### 1. Setup Team Chat Bot + +**Skill**: `zoom-team-chat` + +```javascript +// Handle bot notification +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // Call LLM + const llmResponse = await callClaude(cmd); + + // Check for intents + const intent = parseIntent(llmResponse); + + if (intent.type === 'create_meeting') { + await handleCreateMeeting(toJid, accountId, intent); + } else { + await sendTextMessage(toJid, accountId, llmResponse); + } +} +``` + +### 2. Create Meeting Intent + +**Skills**: `zoom-team-chat` + `zoom-rest-api` + +```javascript +async function handleCreateMeeting(toJid, accountId, intent) { + // Extract meeting details from LLM response + const { topic, start_time, duration } = intent.details; + + // Create meeting using REST API + const meeting = await fetch('https://api.zoom.us/v2/users/me/meetings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${userAccessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + topic, + type: 2, // Scheduled meeting + start_time, + duration + }) + }); + + const meetingData = await meeting.json(); + + // Send meeting details back to Team Chat + await sendChatbotMessage(toJid, accountId, { + head: { text: 'Meeting Created' }, + body: [ + { type: 'message', text: `Meeting "${topic}" created successfully!` }, + { + type: 'fields', + items: [ + { key: 'Start Time', value: start_time }, + { key: 'Duration', value: `${duration} minutes` }, + { key: 'Join URL', value: meetingData.join_url } + ] + }, + { + type: 'actions', + items: [ + { text: 'Join Meeting', value: `join_${meetingData.id}`, style: 'Primary' }, + { text: 'Share Link', value: `share_${meetingData.id}`, style: 'Default' } + ] + } + ] + }); +} +``` + +### 3. LLM Integration with Intent Parsing + +**Skill**: `zoom-team-chat` + +```javascript +const Anthropic = require('@anthropic-ai/sdk'); +const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +async function callClaude(userMessage) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1024, + system: `You are a Zoom assistant bot. You can help users with: +- Creating meetings +- Finding user information +- Answering questions about Zoom +- General assistance + +When a user wants to create a meeting, respond with JSON: +{ + "intent": "create_meeting", + "details": { + "topic": "Meeting topic", + "start_time": "ISO 8601 format", + "duration": 60 + } +} + +Otherwise, provide a helpful text response.`, + messages: [{ role: 'user', content: userMessage }] + }); + + return response.content[0].text; +} + +function parseIntent(llmResponse) { + try { + // Check if response is JSON + if (llmResponse.trim().startsWith('{')) { + const intent = JSON.parse(llmResponse); + return intent; + } + } catch (e) { + // Not JSON, regular text response + } + + return { type: 'text_response', message: llmResponse }; +} +``` + +## Skill Chaining Examples + +### Example 1: Create Meeting from Chat + +**User**: `/bot schedule a team standup tomorrow at 10am for 30 minutes` + +**Flow**: +1. **zoom-team-chat**: Receives command via webhook +2. LLM parses: "create_meeting" intent +3. **zoom-rest-api**: Creates meeting +4. **zoom-team-chat**: Sends confirmation with buttons + +### Example 2: Find User and Start DM + +**User**: `/bot who is John Doe?` + +**Flow**: +1. **zoom-team-chat**: Receives query +2. LLM identifies: "find_user" intent +3. **zoom-rest-api**: Searches users +4. **zoom-team-chat**: Shows user info with "Send DM" button + +### Example 3: Search Chat History + +**User**: `/bot find messages about project alpha` + +**Flow**: +1. **zoom-team-chat**: Receives search query +2. **zoom-rest-api**: Searches chat messages +3. **zoom-team-chat**: Displays results with links + +## Environment Variables + +```bash +# Team Chat (from zoom-team-chat skill) +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# LLM Integration +ANTHROPIC_API_KEY= # or OPENAI_API_KEY + +# Server +PORT=4000 +``` + +## Advanced: Multi-Skill Integration + +### With webhooks + +Subscribe to meeting events and notify in Team Chat: + +```javascript +// Webhook handler for meeting events +app.post('/meeting-webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.started') { + // Notify in Team Chat + await sendChatbotMessage(channelJid, accountId, { + body: [ + { type: 'message', text: `Meeting "${payload.object.topic}" has started!` }, + { + type: 'actions', + items: [ + { text: 'Join Now', value: `join_${payload.object.id}`, style: 'Primary' } + ] + } + ] + }); + } + + res.status(200).send(); +}); +``` + +## Testing Checklist + +- [ ] Bot responds to natural language queries +- [ ] Can create meetings from chat commands +- [ ] Meeting details sent back to Team Chat +- [ ] Buttons trigger appropriate actions +- [ ] LLM intent parsing works correctly +- [ ] Error handling for failed API calls +- [ ] Multi-turn conversation support + +## Resources + +- [zoom-team-chat skill](../../team-chat/SKILL.md) +- [zoom-rest-api skill](../../rest-api/SKILL.md) +- [oauth skill](../../oauth/SKILL.md) +- [Chatbot Setup Example](../../team-chat/examples/chatbot-setup.md) +- [LLM Integration Example](../../team-chat/examples/llm-integration.md) +- [Claude Chatbot Sample](https://github.com/zoom/zoom-chatbot-claude-sample) + +## Next Steps + +1. Build basic chatbot using [Chatbot Setup](../../team-chat/examples/chatbot-setup.md) +2. Add LLM integration +3. Implement intent parsing +4. Add REST API calls for meetings +5. Test end-to-end flow +6. Deploy to production diff --git a/partner-built/zoom-plugin/skills/general/use-cases/testing-development.md b/partner-built/zoom-plugin/skills/general/use-cases/testing-development.md new file mode 100644 index 00000000..6c8023b3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/testing-development.md @@ -0,0 +1,341 @@ +# Testing & Development Environment + +Set up development and testing environments for Zoom integrations. + +## Overview + +Zoom provides several options for development and testing: +- Development app credentials (separate from production) +- Test accounts +- Local webhook testing tools +- SDK sandbox modes + +## Skills Needed + +- **general** - App configuration +- **webhooks** - Webhook testing + +--- + +## Development vs Production Apps + +### Create Separate Apps + +**Always create separate apps for development and production:** + +| Environment | Purpose | Credentials | +|-------------|---------|-------------| +| Development | Testing, debugging | Dev Client ID/Secret | +| Production | Live users | Prod Client ID/Secret | + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Create "MyApp - Development" for testing +3. Create "MyApp - Production" for live deployment +4. Use environment variables to switch between them + +```javascript +// .env.development +ZOOM_CLIENT_ID=dev_client_id_here +ZOOM_CLIENT_SECRET=dev_secret_here +ZOOM_ACCOUNT_ID=dev_account_id + +// .env.production +ZOOM_CLIENT_ID=prod_client_id_here +ZOOM_CLIENT_SECRET=prod_secret_here +ZOOM_ACCOUNT_ID=prod_account_id +``` + +--- + +## Test Accounts + +### Option 1: Developer Account + +Use your own Zoom account for initial development: +- Free tier works for basic API testing +- Pro account needed for SDK testing +- Create test meetings manually + +### Option 2: Zoom Developer Sandbox (ISV Partners) + +ISV partners can request sandbox accounts: +- Contact Zoom partnership team +- Isolated test environment +- Multiple test users + +### Option 3: Programmatic Test Users + +Create test users via API (requires admin account): + +```javascript +// Create a test user +const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', + user_info: { + email: 'testuser+1@yourcompany.com', // Use + alias + type: 1, // Basic user + first_name: 'Test', + last_name: 'User' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} +); +``` + +**Tip**: Use email aliases (`testuser+1@company.com`, `testuser+2@company.com`) that all route to one inbox. + +--- + +## Local Webhook Testing + +### Option 1: ngrok (Recommended) + +Expose your local development webhook server to the internet for testing: + +```bash +# Install ngrok +npm install -g ngrok + +# Start your local server +node server.js # Running on port 3000 + +# In another terminal, create tunnel +ngrok http 3000 +``` + +Output: +``` +Forwarding https://abc123.ngrok.io -> http://YOUR_DEV_HOST:3000 +``` + +Use `https://abc123.ngrok.io/webhook` as your webhook URL in Zoom Marketplace. + +### Option 2: Cloudflare Tunnel + +```bash +# Install cloudflared +brew install cloudflare/cloudflare/cloudflared + +# Create tunnel +LOCAL_WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" +cloudflared tunnel --url "$LOCAL_WEBHOOK_BASE_URL" +``` + +### Option 3: localtunnel + +```bash +npm install -g localtunnel +lt --port 3000 +``` + +### Webhook URL Validation + +Zoom requires validating your webhook endpoint. Your server must respond to the challenge: + +```javascript +app.post('/webhook', (req, res) => { + // Handle Zoom's endpoint validation + if (req.body.event === 'endpoint.url_validation') { + const hashForValidate = crypto + .createHmac('sha256', ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hashForValidate + }); + } + + // Handle actual events + // ... +}); +``` + +--- + +## SDK Development Mode + +### Meeting SDK Web + +Enable debug logging: + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +client.init({ + debug: true, // Enable debug logs + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); +``` + +### Video SDK Web + +```javascript +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'CDN', { + enforceMultipleVideos: true, + stayAwake: true, + patchJsMedia: true, + leaveOnPageUnload: true, +}); + +// Enable debug mode +ZoomVideo.setLogLevel('debug'); +``` + +### Native SDKs (iOS/Android/Desktop) + +Enable verbose logging: + +```swift +// iOS +let initParams = ZoomVideoSDKInitParams() +initParams.enableLog = true +initParams.logFilePrefix = "videosdk_debug" +``` + +```kotlin +// Android +val initParams = ZoomVideoSDKInitParams().apply { + enableLog = true + logFilePrefix = "videosdk_debug" +} +``` + +--- + +## Mock Webhook Events + +### Manual Testing with curl + +Test your webhook handler locally: + +```bash +LOCAL_WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" + +# Simulate meeting.started event +curl -X POST "$LOCAL_WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -H "x-zm-signature: v0=test" \ + -H "x-zm-request-timestamp: $(date +%s)" \ + -d '{ + "event": "meeting.started", + "payload": { + "account_id": "abc123", + "object": { + "id": "123456789", + "uuid": "abcd-1234-efgh", + "topic": "Test Meeting", + "host_id": "xyz789" + } + } + }' +``` + +### Webhook Replay Tool + +Build a simple replay tool for testing: + +```javascript +const fs = require('fs'); + +// Save incoming webhooks to file +app.post('/webhook', (req, res) => { + const filename = `webhooks/${Date.now()}_${req.body.event}.json`; + fs.writeFileSync(filename, JSON.stringify(req.body, null, 2)); + + // Process normally... +}); + +// Replay saved webhook +async function replayWebhook(filename) { + const payload = JSON.parse(fs.readFileSync(filename)); + await processWebhook(payload); +} +``` + +--- + +## Testing Checklist + +### Before Going Live + +- [ ] Test OAuth flow end-to-end +- [ ] Verify webhook signature validation +- [ ] Test with multiple user types (host, participant, admin) +- [ ] Handle rate limiting gracefully +- [ ] Test error scenarios (invalid tokens, network failures) +- [ ] Verify recording download permissions +- [ ] Test SDK on all target platforms +- [ ] Load test with expected user volume + +### API Testing + +```javascript +// Test helper for API calls +async function testAPICall(name, fn) { + console.log(`Testing: ${name}`); + try { + const result = await fn(); + console.log(`Pass: ${name}`); + return result; + } catch (error) { + console.error(`Fail: ${name}:`, error.message); + throw error; + } +} + +// Run tests +await testAPICall('Create meeting', () => + createMeeting({ topic: 'Test' }) +); +await testAPICall('Get meeting', () => + getMeeting(meetingId) +); +await testAPICall('Delete meeting', () => + deleteMeeting(meetingId) +); +``` + +--- + +## Debugging Tips + +### Enable Request Logging + +```javascript +const axios = require('axios'); + +// Log all requests +axios.interceptors.request.use(config => { + console.log(`-> ${config.method.toUpperCase()} ${config.url}`); + return config; +}); + +axios.interceptors.response.use( + response => { + console.log(`<- ${response.status} ${response.config.url}`); + return response; + }, + error => { + console.error(`<- ${error.response?.status} ${error.config?.url}`); + console.error('Error:', error.response?.data); + throw error; + } +); +``` + +### SDK Log Collection + +See [SDK Logs & Troubleshooting](../references/sdk-logs-troubleshooting.md) for collecting SDK debug logs. + +## Resources + +- **ngrok**: https://ngrok.com/ +- **Postman Collection**: https://developers.zoom.us/docs/api/rest/postman/ +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/token-and-scope-troubleshooting.md b/partner-built/zoom-plugin/skills/general/use-cases/token-and-scope-troubleshooting.md new file mode 100644 index 00000000..6747dd0f --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/token-and-scope-troubleshooting.md @@ -0,0 +1,71 @@ +# Token and Scope Troubleshooting (Highest-Frequency Pattern) + +This is the most common failure mode across Zoom REST API integrations and SDK backends: + +- `Invalid access token` +- `Access token is expired` +- `does not contain scopes:[...]` +- "works for me but not for other users/accounts" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-oauth** | Understand which grant type you are using and why | +| 2 | **zoom-rest-api** | Tie the failing endpoint to required scopes and app type | +| 3 | **zoom-webhooks** (optional) | Verify you are validating webhook requests correctly | + +## Triage Checklist + +### 1. Identify Which Token You Are Using + +Ask: + +- Which app type: **Server-to-Server OAuth**, **General App (User OAuth)**, **Chatbot**, **Meeting SDK**, **Video SDK**? +- Which token: user OAuth access token, S2S OAuth access token, bot token, SDK JWT? + +Rule of thumb: + +- REST calls generally require **OAuth access tokens** (S2S or user-based depending on endpoint). +- SDK join flows require **SDK JWT/signature** plus product-specific tokens (for some cases). + +### 2. Confirm the Exact Endpoint and Operation + +Token scope errors are endpoint-specific. Capture: + +- HTTP method + path (example: `GET /v2/users/me/token?type=zak`) +- full error response body from Zoom +- the token’s `scope` string (if present in token response) + +### 3. Map Endpoint -> Required Scopes + +Do not guess scopes. + +- Use `zoom-rest-api` references for the endpoint. +- Use `general/references/authorization-patterns.md` for RBAC and scope validation strategies. + +### 4. Scope Changes Require Re-Consent (User OAuth) + +If you add scopes after users already installed/authorized: + +- existing users may need to reauthorize so the new scopes are granted + +### 5. “Works on My Account” Usually Means One of These + +- different account plan/features enabled +- missing admin role / privilege +- endpoint requires `:admin` scope but token only has user scope +- using the wrong `me` semantics for the app type + +## Common Fix Patterns + +- Add missing scopes, then reauthorize users (User OAuth). +- Ensure you are using the correct grant (S2S for backend automation across an account; User OAuth when acting on behalf of users). +- Validate that the endpoint actually supports your app type (some endpoints are not usable with some token types). + +## Links + +- `../references/authorization-patterns.md` +- `../../rest-api/troubleshooting/token-scope-playbook.md` +- `../../oauth/SKILL.md` + diff --git a/partner-built/zoom-plugin/skills/general/use-cases/transcription-bot-linux.md b/partner-built/zoom-plugin/skills/general/use-cases/transcription-bot-linux.md new file mode 100644 index 00000000..4d05efdc --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/transcription-bot-linux.md @@ -0,0 +1,428 @@ +# Transcription Bot (Linux) + +Build a production-ready transcription bot using Meeting SDK for Linux to automatically transcribe Zoom meetings. + +## Overview + +A transcription bot joins Zoom meetings as a visible participant, captures raw audio, and streams it to a transcription service (AssemblyAI, Whisper, etc.) for real-time or post-meeting transcription. + +## Skills Needed + +- **[meeting-sdk/linux](../../meeting-sdk/linux/SKILL.md)** - Primary (headless meeting bot) +- **[zoom-rest-api](../../rest-api/SKILL.md)** - Get meeting details, OBF tokens +- **[zoom-oauth](../../oauth/SKILL.md)** - JWT token generation for SDK auth + +## Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Meeting │───▶│ Meeting SDK Bot │───▶│ Raw Audio │───▶│ Transcription│ +│ Started │ │ (Linux/Docker) │ │ Stream (PCM) │ │ Service │ +│ │ │ │ │ 32kHz, 16-bit │ │ (AssemblyAI) │ +└──────────────┘ └─────────────────┘ └──────────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ + │ 1. Get OBF Token│ │ 2. StartRaw │ │ 3. Store │ + │ (REST API) │ │ Recording │ │ Transcript│ + └─────────────────┘ └──────────────────┘ └──────────────┘ +``` + +## Implementation Steps + +### Step 1: Generate JWT & OBF Tokens + +**JWT Token** (SDK authentication): +```bash +# Using zoom-oauth skill +curl -X POST https://your-auth-service.com/generate-jwt \ + -d '{"client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET"}' +``` + +**OBF Token** (join external meetings): +```bash +# Via REST API +curl -X POST "https://api.zoom.us/v2/users/{userId}/token?type=obf&ttl=7200" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +See: [zoom-oauth](../../oauth/SKILL.md), [bot-authentication](../../meeting-sdk/references/bot-authentication.md) + +### Step 2: Initialize Meeting SDK + +**Full implementation**: [meeting-sdk/linux](../../meeting-sdk/linux/linux.md) + +```cpp +#include "zoom_sdk.h" +#include + +USING_ZOOM_SDK_NAMESPACE + +// Initialize SDK +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; + +SDKError err = InitSDK(init_params); +if (err != SDKERR_SUCCESS) { + std::cerr << "InitSDK failed: " << err << std::endl; + return 1; +} + +// Authenticate SDK with JWT +AuthContext auth_ctx; +auth_ctx.jwt_token = jwt_token_from_step1; + +IAuthService* auth_service; +CreateAuthService(&auth_service); +auth_service->SetEvent(new MyAuthDelegate()); +auth_service->SDKAuth(auth_ctx); +``` + +### Step 3: Join Meeting + +```cpp +// In onAuthenticationReturn callback +class MyAuthDelegate : public IAuthServiceEvent { + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS) { + JoinParam join_param; + join_param.userType = SDK_UT_WITHOUT_LOGIN; + + auto& params = join_param.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.userName = "Transcription Bot"; + params.psw = meeting_password; + params.isVideoOff = true; // Bot doesn't need video + params.isAudioOff = false; // Need audio for transcription + params.app_privilege_token = obf_token; // From Step 1 + + meeting_service->Join(join_param); + } + } +}; +``` + +### Step 4: Start Raw Recording & Subscribe to Audio + +```cpp +class MyMeetingDelegate : public IMeetingServiceEvent { + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_INMEETING) { + std::cout << "[BOT] Joined meeting successfully" << std::endl; + + // Get recording controller + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Check permission + SDKError can_record = record_ctrl->CanStartRawRecording(); + if (can_record != SDKERR_SUCCESS) { + std::cerr << "[ERROR] Cannot start raw recording: " << can_record << std::endl; + std::cerr << "Need: host/co-host OR recording token" << std::endl; + return; + } + + // Start raw recording (enables raw data access) + SDKError err = record_ctrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "[ERROR] StartRawRecording failed: " << err << std::endl; + return; + } + + std::cout << "[BOT] Raw recording started, subscribing to audio..." << std::endl; + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + if (!audio_helper) { + std::cerr << "[ERROR] Failed to get audio helper" << std::endl; + return; + } + + SDKError audio_err = audio_helper->subscribe(new TranscriptionAudioDelegate()); + if (audio_err != SDKERR_SUCCESS) { + std::cerr << "[ERROR] Audio subscribe failed: " << audio_err << std::endl; + } else { + std::cout << "[BOT] Subscribed to audio successfully" << std::endl; + } + } + } +}; +``` + +### Step 5: Process Audio & Send to Transcription Service + +```cpp +class TranscriptionAudioDelegate : public IZoomSDKAudioRawDataDelegate { +private: + AssemblyAIClient transcription_client; + std::ofstream debug_file; // For debugging: save raw audio + +public: + TranscriptionAudioDelegate() { + // Initialize transcription service connection + transcription_client.connect(); + + // Optional: Save raw audio for debugging + debug_file.open("meeting_audio.pcm", std::ios::binary); + } + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Get audio properties + uint32_t sample_rate = data->GetSampleRate(); // Typically 32000 Hz + uint32_t channels = data->GetChannelNum(); // 1 (mono) or 2 (stereo) + uint32_t buffer_len = data->GetBufferLen(); + char* buffer = data->GetBuffer(); + + // Send to transcription service + transcription_client.send_audio(buffer, buffer_len, sample_rate, channels); + + // Optional: Save for debugging + if (debug_file.is_open()) { + debug_file.write(buffer, buffer_len); + } + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Per-user audio (optional - for speaker diarization) + // node_id identifies the speaker + } + + ~TranscriptionAudioDelegate() { + transcription_client.disconnect(); + if (debug_file.is_open()) { + debug_file.close(); + } + } +}; +``` + +### Step 6: Handle Transcription Results + +```cpp +class AssemblyAIClient { +private: + WebSocketClient ws; + std::string api_key; + +public: + void connect() { + ws.connect("wss://api.assemblyai.com/v2/realtime/ws?sample_rate=32000", { + {"Authorization": api_key} + }); + + // Listen for transcription results + ws.on_message([](const std::string& message) { + json result = json::parse(message); + if (result["message_type"] == "FinalTranscript") { + std::string text = result["text"]; + float confidence = result["confidence"]; + + std::cout << "[TRANSCRIPT] " << text << " (confidence: " << confidence << ")" << std::endl; + + // Store in database + save_to_database(text, timestamp); + } + }); + } + + void send_audio(char* buffer, size_t len, uint32_t sample_rate, uint32_t channels) { + // Convert PCM to base64 (AssemblyAI expects base64-encoded audio) + std::string encoded = base64_encode((unsigned char*)buffer, len); + + json audio_data = { + {"audio_data", encoded} + }; + + ws.send(audio_data.dump()); + } +}; +``` + +## Production Patterns + +### Retry Logic for Meeting Join + +**See**: [meeting-sdk-bot.md](../../meeting-sdk/linux/meeting-sdk-bot.md) + +```cpp +bool joinMeetingWithRetry(int max_attempts = 5, int retry_interval_ms = 60000) { + for (int attempt = 1; attempt <= max_attempts; attempt++) { + std::cout << "[JOIN] Attempt " << attempt << "/" << max_attempts << std::endl; + + SDKError err = meeting_service->Join(join_param); + + if (err == SDKERR_SUCCESS && waitForJoinCallback()) { + std::cout << "[JOIN] Success!" << std::endl; + return true; + } + + if (attempt < max_attempts) { + std::cout << "[JOIN] Retrying in " << (retry_interval_ms / 1000) << "s..." << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(retry_interval_ms)); + } + } + + std::cerr << "[JOIN] Failed after " << max_attempts << " attempts" << std::endl; + return false; +} +``` + +### Docker Deployment + +**Dockerfile**: +```dockerfile +FROM ubuntu:22.04 + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libglib2.0-dev libcurl4-openssl-dev \ + pulseaudio pulseaudio-utils + +# Setup PulseAudio for headless audio +RUN mkdir -p ~/.config && \ + echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf + +# Copy SDK and app +COPY zoom_meeting_sdk/ /app/lib/ +COPY transcription_bot /app/ + +# Setup PulseAudio virtual devices +COPY setup-pulseaudio.sh /app/ +RUN chmod +x /app/setup-pulseaudio.sh + +CMD ["/app/setup-pulseaudio.sh && /app/transcription_bot"] +``` + +**setup-pulseaudio.sh**: +```bash +#!/bin/bash +# Start PulseAudio daemon +pulseaudio --start --exit-idle-time=-1 + +# Create virtual speaker +pactl load-module module-null-sink sink_name=virtual_speaker + +# Create virtual microphone +pactl load-module module-null-sink sink_name=virtual_mic + +echo "PulseAudio configured for headless operation" +``` + +## Configuration Management + +**Environment Variables** (.env): +```bash +# Zoom SDK Credentials +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret + +# Meeting Info +ZOOM_MEETING_NUMBER=1234567890 +ZOOM_MEETING_PASSWORD=abc123 + +# Transcription Service +ASSEMBLYAI_API_KEY=your_api_key + +# Bot Config +BOT_NAME="Transcription Bot" +BOT_JOIN_RETRY_ATTEMPTS=5 +BOT_JOIN_RETRY_INTERVAL_MS=60000 +``` + +**Loading config**: +```cpp +#include + +struct BotConfig { + std::string client_id; + std::string client_secret; + uint64_t meeting_number; + std::string meeting_password; + std::string bot_name; + int join_retry_attempts; + int join_retry_interval_ms; +}; + +BotConfig loadConfig() { + BotConfig cfg; + cfg.client_id = getenv("ZOOM_CLIENT_ID") ?: ""; + cfg.client_secret = getenv("ZOOM_CLIENT_SECRET") ?: ""; + cfg.meeting_number = std::stoull(getenv("ZOOM_MEETING_NUMBER") ?: "0"); + cfg.meeting_password = getenv("ZOOM_MEETING_PASSWORD") ?: ""; + cfg.bot_name = getenv("BOT_NAME") ?: "Transcription Bot"; + cfg.join_retry_attempts = atoi(getenv("BOT_JOIN_RETRY_ATTEMPTS") ?: "5"); + cfg.join_retry_interval_ms = atoi(getenv("BOT_JOIN_RETRY_INTERVAL_MS") ?: "60000"); + return cfg; +} +``` + +## Common Issues & Solutions + +### Issue: Raw Recording Permission Denied + +**Error**: `CanStartRawRecording()` returns `SDKERR_NO_PERMISSION` + +**Solution**: +1. Bot needs to be **host/co-host**, OR +2. Use **recording token** from [REST API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken), OR +3. Host grants recording permission manually + +**See**: [meeting-sdk-bot.md#raw-recording-permission-denied](../../meeting-sdk/linux/meeting-sdk-bot.md#raw-recording-permission-denied) + +### Issue: No Audio in Docker + +**Error**: Audio subscription succeeds but no audio callbacks + +**Solution**: Create `~/.config/zoomus.conf`: +```bash +mkdir -p ~/.config +echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf +``` + +**See**: [linux-reference.md#pulseaudio-setup](../../meeting-sdk/linux/references/linux-reference.md#pulseaudio-setup) + +### Issue: Callbacks Not Firing + +**Error**: `onMeetingStatusChanged()` never called after `Join()` + +**Solution**: Add GLib main loop: +```cpp +#include + +GMainLoop* loop = g_main_loop_new(NULL, FALSE); +g_main_loop_run(loop); // Blocks until quit +``` + +## Related Use Cases + +- **[AI Meeting Assistant](ai-integration.md)** - Add AI analysis on top of transcription +- **[Recording Bot](../../meeting-sdk/linux/concepts/high-level-scenarios.md#scenario-2-recording-bot)** - Record video + audio with sync +- **[Real-time Media Streams](real-time-media-streams.md)** - Alternative: RTMS for invisible bots + +## Related Skills + +- **[meeting-sdk/linux](../../meeting-sdk/linux/SKILL.md)** - Complete Meeting SDK Linux guide +- **[zoom-rest-api](../../rest-api/SKILL.md)** - Get meetings, OBF tokens +- **[zoom-oauth](../../oauth/SKILL.md)** - JWT token generation + +## Sample Code + +**Complete sample**: [meeting-sdk/linux/concepts/high-level-scenarios.md](../../meeting-sdk/linux/concepts/high-level-scenarios.md#scenario-1) + +**Official samples**: +- https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- https://github.com/zoom/meetingsdk-headless-linux-sample + +## Key Takeaways + +✅ **Use OBF tokens** for joining external meetings (require owner present) +✅ **Setup PulseAudio** for Docker/headless audio access +✅ **Call StartRawRecording()** before subscribing to audio +✅ **Use heap memory mode** for raw data (`ZoomSDKRawDataMemoryModeHeap`) +✅ **Implement retry logic** for meeting join (OBF requires owner present) +✅ **Add GLib main loop** for callbacks to work +✅ **Stream audio in real-time** for best transcription latency diff --git a/partner-built/zoom-plugin/skills/general/use-cases/usage-reporting-analytics.md b/partner-built/zoom-plugin/skills/general/use-cases/usage-reporting-analytics.md new file mode 100644 index 00000000..27fbc8bc --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/usage-reporting-analytics.md @@ -0,0 +1,307 @@ +# Usage Reporting & Analytics + +Get meeting statistics, usage reports, and billing data from Zoom. + +## Overview + +Access Zoom's reporting APIs to track meeting usage, participant statistics, and generate analytics for billing, compliance, or business intelligence. + +## Skills Needed + +- **zoom-rest-api** - Primary + +## Report Types + +| Report | Description | +|--------|-------------| +| Daily usage | Meetings per day, minutes used | +| Meeting details | Participant list, join/leave times | +| Webinar reports | Attendee, Q&A, poll data | +| Billing reports | Usage for billing purposes | + +## Prerequisites + +- Admin or owner account +- `report:read` scope + +## Quick Start + +```bash +# Get daily usage report +curl -X GET "https://api.zoom.us/v2/report/daily?year=2024&month=1" \ + -H "Authorization: Bearer {accessToken}" + +# Get meeting participants +curl -X GET "https://api.zoom.us/v2/report/meetings/{meetingId}/participants" \ + -H "Authorization: Bearer {accessToken}" +``` + +## Common Tasks + +### Daily/Monthly Usage Summaries + +```javascript +const axios = require('axios'); + +// Get daily usage report +async function getDailyUsage(year, month) { + const response = await axios.get( + `https://api.zoom.us/v2/report/daily`, + { + params: { year, month }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Returns: dates[], total_meeting_minutes, total_meetings, total_participants + return response.data; +} + +// Aggregate monthly statistics +async function getMonthlyStats(year, month) { + const daily = await getDailyUsage(year, month); + + return { + totalMeetings: daily.dates.reduce((sum, d) => sum + d.meetings, 0), + totalMinutes: daily.dates.reduce((sum, d) => sum + d.meeting_minutes, 0), + totalParticipants: daily.dates.reduce((sum, d) => sum + d.participants, 0), + averageMeetingDuration: daily.dates.length > 0 + ? daily.total_meeting_minutes / daily.total_meetings + : 0, + peakDay: daily.dates.reduce((max, d) => + d.meetings > max.meetings ? d : max, { meetings: 0 } + ) + }; +} + +// Get user-level activity +async function getUserActivity(userId, fromDate, toDate) { + const response = await axios.get( + `https://api.zoom.us/v2/report/users/${userId}/meetings`, + { + params: { from: fromDate, to: toDate, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.meetings; +} +``` + +### Per-Meeting Participant Reports + +```javascript +// Get meeting participants +async function getMeetingParticipants(meetingId) { + // Note: meetingId can be meeting ID or UUID + // If UUID contains / or //, double-encode it + const encodedId = meetingId.includes('/') + ? encodeURIComponent(encodeURIComponent(meetingId)) + : meetingId; + + const response = await axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.participants; +} + +// Calculate meeting metrics +function calculateMeetingMetrics(participants) { + const uniqueParticipants = new Set(participants.map(p => p.user_email || p.name)); + + // Calculate duration per participant + const durations = participants.map(p => { + const join = new Date(p.join_time); + const leave = new Date(p.leave_time); + return (leave - join) / 1000 / 60; // minutes + }); + + return { + totalParticipants: uniqueParticipants.size, + peakConcurrent: calculatePeakConcurrent(participants), + averageAttendanceDuration: average(durations), + lateJoiners: participants.filter(p => /* logic for late join */).length, + earlyLeavers: participants.filter(p => /* logic for early leave */).length + }; +} + +function calculatePeakConcurrent(participants) { + const events = []; + participants.forEach(p => { + events.push({ time: new Date(p.join_time), delta: 1 }); + events.push({ time: new Date(p.leave_time), delta: -1 }); + }); + + events.sort((a, b) => a.time - b.time); + + let current = 0; + let peak = 0; + events.forEach(e => { + current += e.delta; + peak = Math.max(peak, current); + }); + + return peak; +} +``` + +### Webinar Analytics + +```javascript +// Get webinar participants (panelists + attendees) +async function getWebinarReport(webinarId) { + const [participants, absentees, qa, polls] = await Promise.all([ + getWebinarParticipants(webinarId), + getWebinarAbsentees(webinarId), + getWebinarQA(webinarId), + getWebinarPolls(webinarId) + ]); + + return { participants, absentees, qa, polls }; +} + +async function getWebinarParticipants(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/participants`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.participants; +} + +async function getWebinarAbsentees(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/absentees`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.registrants; +} + +async function getWebinarQA(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/qa`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.questions; +} + +async function getWebinarPolls(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/polls`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.questions; +} + +// Calculate webinar engagement score +function calculateEngagementScore(report) { + const { participants, absentees, qa, polls } = report; + + const registeredCount = participants.length + absentees.length; + const attendedCount = participants.length; + const participatedInQA = new Set(qa.map(q => q.email)).size; + const participatedInPolls = new Set(polls.flatMap(p => p.email)).size; + + return { + attendanceRate: (attendedCount / registeredCount * 100).toFixed(1), + qaParticipation: (participatedInQA / attendedCount * 100).toFixed(1), + pollParticipation: (participatedInPolls / attendedCount * 100).toFixed(1), + totalQuestions: qa.length, + averageAttendanceDuration: average(participants.map(p => p.duration)) + }; +} +``` + +### Exporting Data for BI Tools + +```javascript +const { Parser } = require('json2csv'); +const fs = require('fs'); + +// Export to CSV for BI tools +async function exportMeetingsToCSV(fromDate, toDate, outputPath) { + // Get all meetings in date range + const meetings = []; + let nextPageToken = null; + + do { + const response = await axios.get( + 'https://api.zoom.us/v2/report/users/me/meetings', + { + params: { + from: fromDate, + to: toDate, + page_size: 300, + next_page_token: nextPageToken + }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + meetings.push(...response.data.meetings); + nextPageToken = response.data.next_page_token; + } while (nextPageToken); + + // Flatten for CSV + const flatMeetings = meetings.map(m => ({ + id: m.id, + uuid: m.uuid, + topic: m.topic, + start_time: m.start_time, + end_time: m.end_time, + duration_minutes: m.duration, + participants_count: m.participants_count, + host_email: m.host_email, + has_recording: m.has_recording ? 'yes' : 'no' + })); + + const parser = new Parser(); + const csv = parser.parse(flatMeetings); + + fs.writeFileSync(outputPath, csv); + return outputPath; +} + +// Export to JSON for data warehouse +async function exportToDataWarehouse(fromDate, toDate) { + const meetings = await getAllMeetings(fromDate, toDate); + + // Transform for BigQuery/Snowflake + const records = meetings.map(m => ({ + ...m, + _ingested_at: new Date().toISOString(), + _source: 'zoom_api' + })); + + // Send to warehouse + await bigquery.dataset('zoom').table('meetings').insert(records); +} + +// Scheduled export job +const cron = require('node-cron'); + +cron.schedule('0 1 * * *', async () => { + // Run at 1 AM daily + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000); + const from = yesterday.toISOString().split('T')[0]; + const to = from; + + await exportToDataWarehouse(from, to); + console.log(`Exported data for ${from}`); +}); +``` + +## Data Retention Notes + +- **Meeting/Webinar reports**: Available for 12 months +- **Participant reports**: Available for 1 month after meeting ends +- **QSS (Quality of Service)**: Available for 30 days + +## Resources + +- **Reports API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports +- **Dashboard API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Dashboards diff --git a/partner-built/zoom-plugin/skills/general/use-cases/user-and-meeting-creation.md b/partner-built/zoom-plugin/skills/general/use-cases/user-and-meeting-creation.md new file mode 100644 index 00000000..3920df8d --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/user-and-meeting-creation.md @@ -0,0 +1,512 @@ +# User and Meeting Creation Chain + +Create a user account and immediately schedule a meeting for that user. + +## Overview + +A common provisioning pattern: create a new Zoom user via REST API, wait for activation, then create a meeting for that user. This requires understanding user states and proper sequencing. + +## Skills Needed + +| Order | Skill | Purpose | +|-------|-------|---------| +| 1 | **zoom-rest-api** | Create user account | +| 2 | **zoom-rest-api** | Create meeting for user | +| (Optional) | **webhooks** | Receive user activation event | + +## Skill Chaining Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ USER + MEETING CREATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Create User (zoom-rest-api) │ +│ └── POST /users │ +│ └── action: "create" | "autoCreate" | "custCreate" | "ssoCreate" │ +│ └── Returns: user_id, status │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. Wait for User Activation │ +│ └── Option A: Poll GET /users/{userId} until status = "active" │ +│ └── Option B: Listen for user.activated webhook │ +│ └── Option C: Use autoCreate (auto-activates) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Create Meeting (zoom-rest-api) │ +│ └── POST /users/{userId}/meetings │ +│ └── Returns: meeting_id, join_url, start_url │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- Zoom app with Server-to-Server OAuth (recommended) or OAuth +- Scopes: `user:write:admin`, `meeting:write:admin` +- Admin privileges on the Zoom account +- **See [Authorization Patterns](../references/authorization-patterns.md)** for RBAC and permission validation middleware + +## User Action Types + +| Action | Description | Activation | Best For | +|--------|-------------|------------|----------| +| `create` | Sends activation email to user | User clicks email link | Standard provisioning | +| `autoCreate` | Creates pre-activated user | Immediate | Automated systems | +| `custCreate` | Creates user without email | Manual activation | Custom workflows | +| `ssoCreate` | Creates SSO user | SSO login | Enterprise SSO | + +## Step 1: Create User + +### Option A: Standard Creation (with email activation) + +```javascript +const axios = require('axios'); + +/** + * Create a new Zoom user + * @param {Object} userInfo - User information + * @param {string} accessToken - Valid OAuth access token + * @returns {Promise} Created user details + */ +async function createUser(userInfo, accessToken) { + try { + const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', // Sends activation email + user_info: { + email: userInfo.email, + type: userInfo.type || 1, // 1=Basic, 2=Licensed + first_name: userInfo.firstName, + last_name: userInfo.lastName, + password: userInfo.password // Optional + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + email: response.data.email, + first_name: response.data.first_name, + last_name: response.data.last_name, + type: response.data.type, + status: 'pending' // User needs to activate via email + }; + } catch (error) { + if (error.response?.status === 409) { + throw new Error(`User ${userInfo.email} already exists`); + } + if (error.response?.status === 400) { + throw new Error(`Invalid user data: ${error.response.data.message}`); + } + throw error; + } +} +``` + +### Option B: Auto-Create (Immediate activation) + +```javascript +/** + * Create a pre-activated Zoom user (no email required) + * Recommended for automated provisioning + */ +async function createUserAutoActivate(userInfo, accessToken) { + try { + const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'autoCreate', // User is immediately active + user_info: { + email: userInfo.email, + type: userInfo.type || 2, // 2=Licensed (required for autoCreate) + first_name: userInfo.firstName, + last_name: userInfo.lastName, + password: userInfo.password || generateSecurePassword() + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + email: response.data.email, + first_name: response.data.first_name, + last_name: response.data.last_name, + type: response.data.type, + status: 'active' // Ready immediately + }; + } catch (error) { + handleUserCreationError(error, userInfo.email); + } +} + +function generateSecurePassword() { + const crypto = require('crypto'); + return crypto.randomBytes(16).toString('base64') + '!Aa1'; +} +``` + +## Step 2: Wait for User Activation + +### Option A: Polling (Simple) + +```javascript +/** + * Wait for user to become active by polling + * @param {string} userId - User ID to check + * @param {string} accessToken - OAuth token + * @param {number} maxWaitMs - Maximum wait time (default 5 minutes) + * @param {number} pollIntervalMs - Poll interval (default 5 seconds) + */ +async function waitForUserActivation(userId, accessToken, maxWaitMs = 300000, pollIntervalMs = 5000) { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + const user = await getUser(userId, accessToken); + + if (user.status === 'active') { + console.log(`User ${userId} is now active`); + return user; + } + + console.log(`User ${userId} status: ${user.status}, waiting...`); + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + } + + throw new Error(`User ${userId} did not activate within ${maxWaitMs}ms`); +} + +async function getUser(userId, accessToken) { + const response = await axios.get( + `https://api.zoom.us/v2/users/${userId}`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + return response.data; +} +``` + +### Option B: Webhook (Production recommended) + +```javascript +const express = require('express'); +const app = express(); +app.use(express.json()); + +// Store pending user creations +const pendingUsers = new Map(); + +/** + * Create user and wait for webhook activation + */ +async function createUserAndWait(userInfo, accessToken) { + // Create user + const user = await createUser(userInfo, accessToken); + + // Set up promise that resolves when webhook fires + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pendingUsers.delete(user.id); + reject(new Error(`User ${user.id} activation timeout`)); + }, 300000); // 5 minute timeout + + pendingUsers.set(user.id, { resolve, reject, timeout, user }); + }); +} + +// Webhook handler for user activation +app.post('/webhooks/zoom', (req, res) => { + const { event, payload } = req.body; + + if (event === 'user.activated') { + const userId = payload.object.id; + const pending = pendingUsers.get(userId); + + if (pending) { + clearTimeout(pending.timeout); + pendingUsers.delete(userId); + pending.resolve({ ...pending.user, status: 'active' }); + } + } + + res.status(200).send(); +}); +``` + +## Step 3: Create Meeting for User + +```javascript +/** + * Create a meeting for a specific user + * @param {string} userId - User ID or email + * @param {Object} meetingInfo - Meeting details + * @param {string} accessToken - OAuth token + */ +async function createMeetingForUser(userId, meetingInfo, accessToken) { + try { + const response = await axios.post( + `https://api.zoom.us/v2/users/${userId}/meetings`, + { + topic: meetingInfo.topic, + type: meetingInfo.type || 2, // 2 = Scheduled + start_time: meetingInfo.startTime, + duration: meetingInfo.duration || 60, + timezone: meetingInfo.timezone || 'UTC', + agenda: meetingInfo.agenda, + settings: { + host_video: true, + participant_video: true, + join_before_host: false, + mute_upon_entry: true, + waiting_room: true, + ...meetingInfo.settings + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + topic: response.data.topic, + start_time: response.data.start_time, + join_url: response.data.join_url, + start_url: response.data.start_url, + password: response.data.password, + host_id: response.data.host_id, + host_email: response.data.host_email + }; + } catch (error) { + if (error.response?.status === 404) { + throw new Error(`User ${userId} not found or not active`); + } + if (error.response?.status === 429) { + throw new Error('Rate limit exceeded. Try again later.'); + } + throw error; + } +} +``` + +## Complete Chained Operation + +```javascript +/** + * Complete example: Create user and schedule their first meeting + * + * This demonstrates skill chaining: + * 1. zoom-rest-api (users) - Create user + * 2. zoom-rest-api (meetings) - Create meeting + */ + +async function provisionUserWithMeeting(userInfo, meetingInfo) { + console.log('Starting user provisioning...'); + + // Get access token + const accessToken = await getAccessToken(); + + try { + // Step 1: Create user (autoCreate for immediate activation) + console.log(`Creating user: ${userInfo.email}`); + const user = await createUserAutoActivate({ + email: userInfo.email, + firstName: userInfo.firstName, + lastName: userInfo.lastName, + type: 2 // Licensed user + }, accessToken); + + console.log(`User created: ${user.id} (status: ${user.status})`); + + // Step 2: Verify user is active (should be immediate with autoCreate) + if (user.status !== 'active') { + console.log('Waiting for user activation...'); + await waitForUserActivation(user.id, accessToken); + } + + // Step 3: Create meeting for the new user + console.log(`Creating meeting for user: ${user.id}`); + const meeting = await createMeetingForUser(user.id, { + topic: meetingInfo.topic || `${user.first_name}'s Meeting`, + type: 2, + startTime: meetingInfo.startTime || new Date(Date.now() + 3600000).toISOString(), + duration: meetingInfo.duration || 60, + timezone: meetingInfo.timezone || 'America/Los_Angeles' + }, accessToken); + + console.log(`Meeting created: ${meeting.id}`); + + // Return complete provisioning result + return { + success: true, + user: { + id: user.id, + email: user.email, + name: `${user.first_name} ${user.last_name}` + }, + meeting: { + id: meeting.id, + topic: meeting.topic, + join_url: meeting.join_url, + start_url: meeting.start_url, + start_time: meeting.start_time + } + }; + + } catch (error) { + console.error('Provisioning failed:', error.message); + + // Cleanup: If user was created but meeting failed, optionally delete user + // await deleteUser(user.id, accessToken); + + return { + success: false, + error: error.message + }; + } +} + +// Helper: Get access token (Server-to-Server OAuth) +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +// Usage +const result = await provisionUserWithMeeting( + { + email: 'newuser@example.com', + firstName: 'John', + lastName: 'Doe' + }, + { + topic: 'Onboarding Meeting', + startTime: '2024-02-01T10:00:00Z', + duration: 30 + } +); + +console.log(result); +// { +// success: true, +// user: { id: 'abc123', email: 'newuser@example.com', name: 'John Doe' }, +// meeting: { id: '123456789', topic: 'Onboarding Meeting', join_url: '...', ... } +// } +``` + +## Error Handling + +### Error Recovery Pattern + +```javascript +/** + * Robust provisioning with rollback capability + */ +async function provisionUserWithMeetingSafe(userInfo, meetingInfo) { + const accessToken = await getAccessToken(); + let createdUser = null; + + try { + // Step 1: Create user + createdUser = await createUserAutoActivate(userInfo, accessToken); + + // Step 2: Create meeting + const meeting = await createMeetingForUser(createdUser.id, meetingInfo, accessToken); + + return { success: true, user: createdUser, meeting }; + + } catch (error) { + // Rollback: Delete user if meeting creation failed + if (createdUser && error.message.includes('meeting')) { + console.log(`Rolling back: deleting user ${createdUser.id}`); + try { + await deleteUser(createdUser.id, accessToken); + } catch (deleteError) { + console.error('Rollback failed:', deleteError.message); + } + } + + throw error; + } +} + +async function deleteUser(userId, accessToken) { + await axios.delete( + `https://api.zoom.us/v2/users/${userId}?action=delete`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); +} +``` + +### Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| 409 Conflict | User email already exists | Use existing user or different email | +| 400 Bad Request | Invalid user data | Check email format, required fields | +| 404 Not Found | User not active/found | Wait for activation or verify user ID | +| 429 Rate Limit | Too many requests | Implement backoff, batch operations | +| 201 but pending | User needs to activate | Use autoCreate or wait for activation | + +## User Types Reference + +| Type | Value | Description | Can Host? | +|------|-------|-------------|-----------| +| Basic | 1 | Free user | Limited | +| Licensed | 2 | Paid license | Yes | +| On-prem | 3 | On-premise deployment | Yes | +| None | 99 | No license | No | + +## Best Practices + +1. **Use autoCreate for automation** - Avoids waiting for email activation +2. **Implement rollback logic** - Clean up if later steps fail +3. **Cache access tokens** - Tokens are valid for 1 hour +4. **Handle rate limits** - Implement exponential backoff +5. **Validate input early** - Check email format before API calls +6. **Log all operations** - Aids debugging and audit + +## Related Use Cases + +- **[Authorization Patterns](../references/authorization-patterns.md)** - RBAC, permission validation, and scope checking for multi-step workflows +- **[Meeting Automation](meeting-automation.md)** - More meeting management patterns +- **[Meeting Details with Events](meeting-details-with-events.md)** - Track meeting events +- **[Recording & Transcription](recording-transcription.md)** - Handle recordings + +## Resources + +- **Users API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Users +- **Meetings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **User Types**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userCreate diff --git a/partner-built/zoom-plugin/skills/general/use-cases/video-sdk-bring-your-own-storage.md b/partner-built/zoom-plugin/skills/general/use-cases/video-sdk-bring-your-own-storage.md new file mode 100644 index 00000000..22b419ac --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/video-sdk-bring-your-own-storage.md @@ -0,0 +1,220 @@ +# BYOS (Bring Your Own Storage) + +Video SDK feature that saves cloud recordings **directly** to your Amazon S3 bucket. No downloading required. + +> **Official docs:** https://developers.zoom.us/docs/build/storage/ + +## How BYOS Works + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Video SDK │ │ Your AWS │ +│ Session │ Direct Upload │ S3 Bucket │ +│ │ ──────────────────►│ │ +│ Recording... │ (via IAM role │ .mp4 .m4a .txt │ +│ │ or access key) │ │ +└─────────────────┘ └─────────────────┘ +``` + +## BYOS vs Recording Download Pipeline + +| Aspect | BYOS (Video SDK) | Recording Download Pipeline | +|--------|------------------|----------------------------| +| How it works | Zoom writes directly to your S3 | You download from Zoom, upload to S3 | +| Products | Video SDK only | Zoom Meetings | +| Latency | During recording | After recording completes | +| Your infrastructure | Just S3 bucket + credentials | Webhook server + download code | +| Bandwidth cost | None (direct to S3) | You pay for download | + +## Prerequisites + +- Video SDK account with **Cloud Recording add-on plan** (Universal Credit plans include this) +- AWS account with administrator access +- Amazon S3 bucket + +## S3 File Path Structure + +BYOS recordings are stored at: + +``` +Buckets/{bucketName}/cmr/byos/{YYYY}/{MM}/{DD}/{GUID}/cmr_byos/ +``` + +## Setup + +### Step 1: Create S3 Bucket + +Create a private S3 bucket with "Block all public access" enabled. + +### Step 2: Enable BYOS in Zoom Portal + +1. Go to **Developer account web portal** +2. Navigate to **Account Settings** → **General** → **Communications Content Storage Location** +3. Toggle **Bring Your Own Storage** on +4. Click **Manage Storage** → **Add Storage** + +### Step 3: Choose Authentication Method + +#### Option A: AWS Access Key (Simpler) + +1. Enter your **Access Key ID** and **Access Secret Key** +2. Zoom encrypts these values +3. Click **Save** + +#### Option B: Cross Account Access (More Secure) + +1. **Enter Your ARN** in format: + ``` + arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ZoomArchivingRole + ``` + +2. **Get Zoom Account ID** from the help text below the ARN field, or via [Get Account Settings API](https://developers.zoom.us/docs/api/accounts/#tag/accounts/get/accounts/{accountId}/settings) + +3. **Create IAM Policy** with these permissions: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "S3BucketList", + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": "arn:aws:s3:::your_bucket_name" + }, + { + "Sid": "S3ObjectAccess", + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::your_bucket_name/*" + } + ] +} +``` + +4. **Create Trust Relationship** for the IAM role: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "Zoom_ARN" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "YOUR_ZOOM_ACCOUNT_ID" + } + } + } + ] +} +``` + +### Step 4: Verify Configuration + +Zoom performs HTTP PUT, GET, and LIST operations against your bucket to validate credentials. + +### Step 5: Test + +Record a Video SDK session and verify recordings appear in your S3 bucket. + +## Managing Storage Locations + +- **Multiple locations:** You can add multiple storage locations, but only one is the default +- **Switch default:** Use the ellipsis menu to change the default location +- **Delete:** Cannot delete the only storage location; add another first + +### Via API + +Use the [Video SDK BYOS Storage APIs](https://developers.zoom.us/docs/api/video-sdk/#tag/byos-storage): + +| Endpoint | Description | +|----------|-------------| +| `GET /v2/videosdk/byos/storage` | List storage locations | +| `POST /v2/videosdk/byos/storage` | Add storage location | +| `DELETE /v2/videosdk/byos/storage/{storageId}` | Delete storage location | +| `PATCH /v2/videosdk/byos/storage/{storageId}` | Update storage location | + +## Managing Recordings + +Use the [Cloud Recording APIs](https://developers.zoom.us/docs/api/video-sdk/#tag/cloud-recording) to manage, play, and download BYOS recordings. + +**Important:** Cloud recordings have two components: + +| Component | Location | Managed by | +|-----------|----------|------------| +| Metadata | Zoom (portal) | Zoom APIs / Web Portal | +| Recording files | Your S3 bucket | You / AWS | + +## Web Portal Limitations + +The Zoom web portal only manages **metadata**, not S3 files: + +- **Delete in portal** → Only removes metadata, S3 files remain +- **Trash recovery** → Restore metadata within 30 days to re-enable playback +- **After 30 days** → Metadata permanently deleted, no portal playback (files still in S3) + +**Recommendation:** Use APIs for full control over BYOS recordings. + +## Effects of Disabling BYOS + +| Action | Metadata | S3 Files | +|--------|----------|----------| +| Toggle BYOS off | Deleted from Recordings page | Unaffected | +| Delete storage location | Deleted from Recordings page | Unaffected | + +**Warning:** Both actions permanently remove metadata. Re-adding the same storage location won't restore playback in the portal. + +## Troubleshooting + +### Verify Storage Location + +1. Click **Manage Storage** +2. Click **ellipsis** → **Verify** + +This tests region, bucket, and credentials. + +### Check AWS CloudTrail + +Look for `AssumeRole` or `PutObject` errors: + +```bash +aws cloudtrail lookup-events \ + --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole +``` + +### Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| Upload fails | Invalid credentials | Verify access key or IAM role | +| Permission denied | Missing S3 permissions | Check IAM policy has all required actions | +| Bucket not found | Wrong region/name | Verify bucket name and region match exactly | + +## Security Best Practices + +1. **Use Cross Account Access** over access keys when possible +2. **Set External ID** to your Zoom account ID (prevents confused deputy) +3. **Enable S3 encryption** (SSE-S3 or SSE-KMS) +4. **Enable bucket versioning** for recovery +5. **Audit IAM roles** regularly for least privilege +6. **Delete during off-peak hours** to avoid incomplete uploads + +## Resources + +- **BYOS Overview:** https://developers.zoom.us/docs/build/storage/ +- **Get Started:** https://developers.zoom.us/docs/build/storage-get-started/ +- **Manage Storage:** https://developers.zoom.us/docs/build/storage-manage/ +- **BYOS Storage APIs:** https://developers.zoom.us/docs/api/video-sdk/#tag/byos-storage +- **Cloud Recording APIs:** https://developers.zoom.us/docs/api/video-sdk/#tag/cloud-recording diff --git a/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md b/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md new file mode 100644 index 00000000..07cc9d37 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md @@ -0,0 +1,32 @@ +# Virtual Agent Campaign Web and Mobile Wrapper + +Use this flow when you want one Virtual Agent campaign strategy across website and native mobile wrappers. + +## When to Use + +- You already run campaign-based web embed and need consistent mobile behavior. +- You need native app callbacks for exit or support handoff while keeping web bot logic. +- You want to avoid rebuilding bot UI natively on each platform. + +## Skill Chain + +1. [virtual-agent](../../virtual-agent/SKILL.md) +2. [virtual-agent/web](../../virtual-agent/web/SKILL.md) +3. [virtual-agent/android](../../virtual-agent/android/SKILL.md) or [virtual-agent/ios](../../virtual-agent/ios/SKILL.md) +4. [contact-center](../../contact-center/SKILL.md) + +## Typical Flow + +1. Configure campaign targeting and publish bot flow. +2. Validate web behavior with campaign controls and event listeners. +3. Embed the same campaign URL in Android/iOS WebView containers. +4. Inject bridge handlers for exit, common commands, and `support_handoff`. +5. Apply URL governance (`_self`, `_blank`, `window.open`) consistently. +6. Release with shared monitoring for engagement start/end metrics. + +## References + +- [Virtual Agent Root Skill](../../virtual-agent/SKILL.md) +- [Web Lifecycle and Events](../../virtual-agent/web/concepts/lifecycle-and-events.md) +- [Android JS Bridge Patterns](../../virtual-agent/android/examples/js-bridge-patterns.md) +- [iOS JS Bridge Patterns](../../virtual-agent/ios/examples/js-bridge-patterns.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md b/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md new file mode 100644 index 00000000..96c0e86f --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md @@ -0,0 +1,30 @@ +# Virtual Agent Knowledge Base Sync Pipeline + +Use this flow when knowledge content lives outside Zoom and must stay synchronized for Virtual Agent responses. + +## When to Use + +- Your source-of-truth KB is in another CMS. +- Web sync alone is not enough for your content structure. +- You need repeatable ingestion and update automation. + +## Skill Chain + +1. [virtual-agent](../../virtual-agent/SKILL.md) +2. [zoom-rest-api](../../rest-api/SKILL.md) +3. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Decide sync mode: sitemap/link-discovery/manual URLs vs custom API connector. +2. Configure S2S OAuth app and required scopes. +3. Pull content from external source and transform to KB article schema. +4. Upsert articles, tags, and categories into Virtual Agent knowledge base. +5. Reconcile stale entries and monitor sync errors. +6. Re-run sync on release cadence. + +## References + +- [Virtual Agent Environment Variables](../../virtual-agent/references/environment-variables.md) +- [Virtual Agent Troubleshooting](../../virtual-agent/troubleshooting/common-drift-and-breaks.md) +- [REST API Skill](../../rest-api/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/general/use-cases/web-sdk-embedding.md b/partner-built/zoom-plugin/skills/general/use-cases/web-sdk-embedding.md new file mode 100644 index 00000000..6ee94d1b --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/web-sdk-embedding.md @@ -0,0 +1,207 @@ +# Web SDK Embedding + +Embed Zoom SDKs in iframes with proper cross-origin configuration. + +## Overview + +Configure your web application to properly embed Zoom Meeting SDK or Video SDK, including iframe setup, CORS headers, and cross-origin requirements. + +## Skills Needed + +- **zoom-meeting-sdk** (Web) +- **zoom-video-sdk** (Web) + +## Embedding Options + +| Option | Description | +|--------|-------------| +| Same-origin | SDK loaded in main page | +| iframe (same-origin) | SDK in iframe, same domain | +| iframe (cross-origin) | SDK in iframe, different domain | + +## Required Headers + +For cross-origin embedding with SharedArrayBuffer: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +## iframe Configuration + +```html + +``` + +## Common Tasks + +### Basic iframe Embedding + +```html + + +``` + +### Cross-Origin Setup with SharedArrayBuffer + +SharedArrayBuffer is required for: +- 720p sending +- Virtual backgrounds +- Gallery view + +**Server headers (Node.js/Express)**: +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +**Nginx config**: +```nginx +location / { + add_header Cross-Origin-Opener-Policy same-origin; + add_header Cross-Origin-Embedder-Policy require-corp; +} +``` + +**Cloudflare Workers**: +```javascript +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + const response = await fetch(request); + const newResponse = new Response(response.body, response); + newResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin'); + newResponse.headers.set('Cross-Origin-Embedder-Policy', 'require-corp'); + return newResponse; +} +``` + +### Permission Handling + +```javascript +// Request permissions before joining +async function requestMediaPermissions() { + try { + await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + return true; + } catch (err) { + console.error('Permission denied:', err); + return false; + } +} + +// Check permission state +async function checkPermissions() { + const camera = await navigator.permissions.query({ name: 'camera' }); + const microphone = await navigator.permissions.query({ name: 'microphone' }); + + return { + camera: camera.state, // 'granted', 'denied', 'prompt' + microphone: microphone.state + }; +} +``` + +### Communication Between Parent and iframe + +**From parent to iframe**: +```javascript +// Parent page +const iframe = document.getElementById('zoom-frame'); +iframe.contentWindow.postMessage({ + type: 'JOIN_MEETING', + meetingNumber: '123456789', + password: 'pass' +}, 'https://your-app.com'); +``` + +**From iframe to parent**: +```javascript +// Inside iframe (meeting page) +window.parent.postMessage({ + type: 'MEETING_STATUS', + status: 'joined' +}, '*'); +``` + +**Receive messages**: +```javascript +// In parent or iframe +window.addEventListener('message', (event) => { + // Verify origin + if (event.origin !== 'https://trusted-domain.com') return; + + const { type, ...data } = event.data; + + switch (type) { + case 'MEETING_STATUS': + handleMeetingStatus(data); + break; + case 'LEAVE_MEETING': + handleLeaveMeeting(); + break; + } +}); +``` + +### Mobile Responsive Embedding + +```html + + +
+ +
+``` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Camera/mic blocked | Check `allow` attribute | +| SharedArrayBuffer error | Add COOP/COEP headers | +| Cross-origin errors | Configure CORS properly | + +## Resources + +- **Meeting SDK Web**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Video SDK Web**: https://developers.zoom.us/docs/video-sdk/web/ diff --git a/partner-built/zoom-plugin/skills/general/use-cases/zoom-phone-smart-embed-crm.md b/partner-built/zoom-plugin/skills/general/use-cases/zoom-phone-smart-embed-crm.md new file mode 100644 index 00000000..3365fe25 --- /dev/null +++ b/partner-built/zoom-plugin/skills/general/use-cases/zoom-phone-smart-embed-crm.md @@ -0,0 +1,31 @@ +# Zoom Phone Smart Embed CRM Integration + +Build CRM communication workflows by combining Zoom Phone Smart Embed, OAuth-authenticated Phone APIs, and webhook/event ingestion. + +## Skills Needed + +- `phone` (primary) +- `zoom-oauth` +- `zoom-rest-api` +- `zoom-webhooks` + +## Core Architecture + +1. Authenticate users/admins with OAuth. +2. Embed Zoom Phone in CRM side panel. +3. Capture Smart Embed events and correlate to CRM records. +4. Fetch call history/call element details from Phone APIs. +5. Store call outcomes, notes, and follow-up tasks. + +## High-Value Use Cases + +- Click-to-call from account/contact table. +- Post-call disposition and notes sync. +- SMS follow-up with status tracking. +- Real-time supervisor dashboards using webhook updates. + +## Where to Go Next + +- `../../phone/SKILL.md` +- `../../phone/RUNBOOK.md` +- `../../phone/references/deprecations-and-migrations.md` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/RUNBOOK.md new file mode 100644 index 00000000..d22f4503 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/RUNBOOK.md @@ -0,0 +1,72 @@ +# Meeting SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches high-frequency Meeting SDK failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Integration Mode + +- Web Client View (CDN/global `ZoomMtg`) or Web Component View (npm `ZoomMtgEmbedded`). +- Do not mix APIs between modes. + +## 2) Confirm Signature Path + +- Generate signature server-side with SDK Secret. +- Never expose SDK Secret in browser code. +- Confirm `meetingNumber` and `role` in signature payload match join request. + +## 3) Confirm Join Payload Hygiene + +- Pass only valid values; avoid undefined optional fields. +- Ensure meeting number is normalized as digits string. +- If rendering issues appear, test with safer default view settings. + +## 4) Confirm Browser + Security Prereqs + +- If using advanced media features, validate cross-origin isolation setup (COOP/COEP) when required. +- Avoid global CSS resets that break Zoom UI layouts. +- Ensure page overlays/z-index do not hide meeting container. + +## 5) Confirm Routing and Base Path + +- Signature endpoint must be reachable from frontend (same origin proxy recommended). +- In subpath deployments, verify fetch URLs and reverse proxy rewrites. + +## 6) Quick Probes + +- Signature endpoint returns JSON with non-empty signature. +- Join call returns actionable SDK errors (not generic 404 HTML). +- Browser console has no obvious mixed-content/CORS blocks. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify signature endpoint responds with JSON +curl -sS -i "$MEETING_SDK_BASE_URL/api/signature" + +# 2) Verify app page is reachable and returns HTML +curl -sS -i "$MEETING_SDK_BASE_URL" +``` + +Expected: endpoints return valid JSON/HTML (not generic 404/502 pages). + +## 7) Fast Decision Tree + +- **Black/blank UI** -> check CSS/z-index, mode mismatch, and payload field hygiene. +- **Join fails quickly** -> signature payload mismatch or expired signature. +- **Intermittent load issues** -> cross-origin isolation or browser extension interference. + +## 8) SDK Selection Guardrail + +- Use Meeting SDK when embedding Zoom meeting experiences. +- Use Video SDK when building fully custom video UX. + +## 9) Wrong-Path Detector (SDK vs REST) + +- If implementation is producing `join_url` links instead of SDK join calls, you are on REST path. +- If code depends on `GET/POST /v2/meetings` but user asked for embedded in-app join UX, you are on wrong path. +- For Meeting SDK MVP, require: signature endpoint + frontend `ZoomMtg`/`ZoomMtgEmbedded` join. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/SKILL.md new file mode 100644 index 00000000..64b72ea9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/SKILL.md @@ -0,0 +1,255 @@ +--- +name: build-zoom-meeting-sdk-app +description: Reference skill for Zoom Meeting SDK. Use after routing to a meeting-embed workflow when implementing real Zoom meeting joins, platform-specific SDK behavior, auth and join flows, waiting room issues, or meeting bot patterns. +triggers: + - "embed meeting" + - "embed zoom meeting" + - "integrate meeting" + - "meeting in web app" + - "meeting in website" + - "add zoom to website" + - "meeting sdk" + - "join meeting programmatically" + - "meeting bot" + - "bot joins meeting" + - "joining meeting timeout" + - "join meeting failed" + - "waiting room" + - "hide meeting info" + - "hide meeting password" +--- + +# /build-zoom-meeting-sdk-app + +Background reference for embedded Zoom meetings across web, mobile, desktop, and Linux bot environments. Prefer `build-zoom-meeting-app` or `build-zoom-bot` first, then route here for platform detail. + +# Zoom Meeting SDK + +Embed the full Zoom meeting experience into web, mobile, desktop, and headless integrations. + +## Hard Routing Guardrail (Read First) + +- If the user asks to embed/join meetings inside their app UI, route to Meeting SDK implementation. +- Do not switch to REST-only meeting link flow unless the user explicitly asks for meeting resource management or browser `join_url` links. +- Meeting SDK join path requires SDK signature + SDK join call; REST `join_url` is not a Meeting SDK join payload. + +## Prerequisites + +- Zoom app with Meeting SDK credentials +- SDK Key and Secret from Marketplace +- Platform-specific development environment (Web, Android, iOS, macOS, Unreal, Electron, Linux, or Windows) + +> **Need help with OAuth or signatures?** See the **[zoom-oauth](../oauth/SKILL.md)** skill for authentication flows. + +> **Need pre-join diagnostics on web?** Use **[probe-sdk](../probe-sdk/SKILL.md)** before Meeting SDK init/join to gate low-readiness devices/networks. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](RUNBOOK.md)** before deep debugging. + +## Quick Start (Web - Client View via CDN) + +```html + + + + + + + + +``` + +## Critical Notes (Web) + +### 1. CDN vs npm - Different APIs! + +| Distribution | Global Object | View Type | API Style | +|--------------|---------------|-----------|-----------| +| CDN (`zoom-meeting-{ver}.min.js`) | `ZoomMtg` | Client View (full-page) | Callbacks | +| npm (`@zoom/meetingsdk`) | `ZoomMtgEmbedded` | Component View (embeddable) | Promises | + +### 2. Backend Required for Production + +**Never expose SDK Secret in client code.** Generate signatures server-side: + +```javascript +// server.js (Node.js example) +const KJUR = require('jsrsasign'); + +app.post('/api/signature', (req, res) => { + const { meetingNumber, role } = req.body; + const iat = Math.floor(Date.now() / 1000) - 30; + const exp = iat + 60 * 60 * 2; + + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { + sdkKey: process.env.ZOOM_SDK_KEY, + mn: String(meetingNumber).replace(/\D/g, ''), + role: parseInt(role, 10), + iat, exp, tokenExp: exp + }; + + const signature = KJUR.jws.JWS.sign('HS256', + JSON.stringify(header), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); + + res.json({ signature, sdkKey: process.env.ZOOM_SDK_KEY }); +}); +``` + +### 3. CSS Conflicts - Avoid Global Resets + +Global `* { margin: 0; }` breaks Zoom's UI. Scope your styles: + +```css +/* BAD */ +* { margin: 0; padding: 0; } + +/* GOOD */ +.your-app, .your-app * { box-sizing: border-box; } +``` + +### 4. Client View Toolbar Cropping Fix + +If toolbar falls off screen, scale down the Zoom UI: + +```css +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + /* Critical for SPAs (React/Next/etc): ensure Zoom UI isn't behind your app shell/overlays. */ + z-index: 9999 !important; + transform: scale(0.95) !important; + transform-origin: top center !important; +} +``` + +### 5. Hide Your App When Meeting Starts + +Client View takes over full page. Hide your UI: + +```javascript +// In ZoomMtg.init success callback: +document.documentElement.classList.add('meeting-active'); +document.body.classList.add('meeting-active'); +``` + +```css +body.meeting-active .your-app { display: none !important; } +body.meeting-active { background: #000 !important; } +``` + +## UI Options (Web) + +Meeting SDK provides **Zoom's UI with customization options**: + +| View | Description | +|------|-------------| +| **Component View** | Extractable, customizable UI - embed meeting in a div | +| **Client View** | Full-page Zoom UI experience | + +**Note**: Unlike Video SDK where you build the UI from scratch, Meeting SDK uses Zoom's UI as the base with customization on top. + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| SDK Key/Secret | Credentials from Marketplace | +| Signature | JWT signed with SDK Secret | +| Component View | Extractable, customizable UI (Web) | +| Client View | Full-page Zoom UI (Web) | + +## Detailed References + +### Platform Guides +- **[android/SKILL.md](android/SKILL.md)** - Android SDK (default/custom UI, join/start/auth lifecycle, mobile integration) +- **[android/references/android-reference-map.md](android/references/android-reference-map.md)** - Android API surface map and drift watchpoints +- **[ios/SKILL.md](ios/SKILL.md)** - iOS SDK (default/custom UI, join/start/auth lifecycle, mobile integration) +- **[ios/references/ios-reference-map.md](ios/references/ios-reference-map.md)** - iOS API surface map and drift watchpoints +- **[macos/SKILL.md](macos/SKILL.md)** - macOS SDK (desktop default/custom UI, service controllers, host flows) +- **[macos/references/macos-reference-map.md](macos/references/macos-reference-map.md)** - macOS API surface map and drift watchpoints +- **[unreal/SKILL.md](unreal/SKILL.md)** - Unreal Engine wrapper (C++/Blueprint wrapper behavior and SDK mapping) +- **[unreal/references/unreal-reference-map.md](unreal/references/unreal-reference-map.md)** - Unreal wrapper reference map and version-lag notes +- **[references/android.md](references/android.md)** - Android pointer doc for fast routing from broad Meeting SDK queries +- **[references/ios.md](references/ios.md)** - iOS pointer doc for fast routing from broad Meeting SDK queries +- **[references/macos.md](references/macos.md)** - macOS pointer doc for fast routing from broad Meeting SDK queries +- **[references/unreal.md](references/unreal.md)** - Unreal pointer doc for fast routing from broad Meeting SDK queries +- **[linux/SKILL.md](linux/SKILL.md)** - Linux SDK headless bot skill entrypoint +- **[linux/linux.md](linux/linux.md)** - Linux SDK (C++ headless bots, raw media access) +- **[linux/references/linux-reference.md](linux/references/linux-reference.md)** - Linux dependencies, Docker, troubleshooting +- **[react-native/SKILL.md](react-native/SKILL.md)** - React Native SDK (iOS/Android wrapper, join/start flows, bridge setup) +- **[react-native/SKILL.md](react-native/SKILL.md)** - React Native complete navigation +- **[electron/SKILL.md](electron/SKILL.md)** - Electron SDK (desktop wrapper, auth/join flows, module controllers, raw data) +- **[electron/SKILL.md](electron/SKILL.md)** - Electron complete navigation +- **[windows/SKILL.md](windows/SKILL.md)** - Windows SDK (C++ desktop applications, raw media access) +- **[windows/references/windows-reference.md](windows/references/windows-reference.md)** - Windows dependencies, Visual Studio setup, troubleshooting +- **[web/references/web.md](web/references/web.md)** - Web SDK (Component + Client View) +- **[web/references/web-tracking-id.md](web/references/web-tracking-id.md)** - Tracking ID configuration + +### Features +- **[references/authorization.md](references/authorization.md)** - SDK JWT generation +- **[references/bot-authentication.md](references/bot-authentication.md)** - ZAK vs OBF vs JWT tokens for bots +- **[references/breakout-rooms.md](references/breakout-rooms.md)** - Programmatic breakout room management +- **[references/ai-companion.md](references/ai-companion.md)** - AI Companion controls in meetings +- **[references/webinars.md](references/webinars.md)** - Webinar SDK features +- **[references/forum-top-questions.md](references/forum-top-questions.md)** - Common forum question patterns (what to cover) +- **[references/triage-intake.md](references/triage-intake.md)** - What to ask first (turn vague reports into answers) +- **[references/signature-playbook.md](references/signature-playbook.md)** - Signature/root-cause playbook +- **[references/multiple-meetings.md](references/multiple-meetings.md)** - Joining multiple meetings / multiple instances +- **[references/troubleshooting.md](references/troubleshooting.md)** - Common issues and solutions + +## Sample Repositories + +### Official (by Zoom) + +| Type | Repository | Stars | +|------|------------|-------| +| Linux Headless | [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | 4 | +| Linux Raw Data | [meetingsdk-linux-raw-recording-sample](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | 0 | +| Web | [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | 643 | +| Web NPM | [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | 324 | +| React | [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | 177 | +| Auth | [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | 124 | +| Angular | [meetingsdk-angular-sample](https://github.com/zoom/meetingsdk-angular-sample) | 60 | +| Vue.js | [meetingsdk-vuejs-sample](https://github.com/zoom/meetingsdk-vuejs-sample) | 42 | + +**Full list**: See [general/references/community-repos.md](../general/references/community-repos.md) + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/RUNBOOK.md new file mode 100644 index 00000000..3df8fc14 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Android 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Android (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/android/ +- https://marketplacefront.zoom.us/sdk/meeting/android/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/android/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/android/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/SKILL.md new file mode 100644 index 00000000..9821d830 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/SKILL.md @@ -0,0 +1,46 @@ +--- +name: zoom-meeting-sdk-android +description: | + Zoom Meeting SDK for Android native apps. Use when embedding Zoom meetings in Android with + default/custom UI, PKCE + SDK auth, join/start flows, and Meeting SDK API integration. +user-invocable: false +triggers: + - "meeting sdk android" + - "zoom android sdk" + - "android default ui" + - "android custom ui" + - "join meeting android" + - "start meeting android" +--- + +# Zoom Meeting SDK (Android) + +Use this skill when building Android apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [android.md](android.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/android-reference-map.md](references/android-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Routing Notes + +- Use **default UI** first for first successful join/start validation. +- Move to **custom UI** once auth, meeting state transitions, and permissions are stable. +- For signature/JWT mistakes, chain with [../../oauth/SKILL.md](../../oauth/SKILL.md) and [../references/signature-playbook.md](../references/signature-playbook.md). + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/android/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/android/index.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/android.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/android.md new file mode 100644 index 00000000..fbdacb64 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/android.md @@ -0,0 +1,19 @@ +# Meeting SDK Android Guide + +## Scope + +Android Meeting SDK integration for default UI, custom UI, auth, start/join, and in-meeting feature modules. + +## Validation Snapshot + +- Crawled docs path includes: `get-started`, `integrate`, `start-join-mtg-webinar`, `default-ui`, `custom-ui`, `resource/error-codes`. +- API reference snapshot includes class/interface/function maps from `index.html`, `annotated.html`, `classes.html`, `files.html`, and `functions*` pages. +- Local package checked: `zoom-sdk-android-6.7.5.37500` (contains `mobilertc.aar`, sample apps, dynamic sample modules). + +## Practical Guidance + +1. Initialize and authenticate SDK. +2. Get first successful join in default UI. +3. Add feature flags/settings and error handling. +4. Move to custom UI only for required UX control. +5. Add observability for meeting status and SDK callback failures. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/architecture.md new file mode 100644 index 00000000..2f7ae63b --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/architecture.md @@ -0,0 +1,23 @@ +# Android Architecture + +## Layer Model + +- App UI layer (Activity/Fragment/Compose wrapper). +- Meeting orchestration layer (init/auth/join/start state machine). +- SDK facade layer (`mobilertc.aar` interfaces and controllers). +- Backend signing service (short-lived signature/JWT issuance). + +## Reference Flow + +```text +Android UI -> App Meeting Service -> Backend Signature API -> Zoom Meeting SDK + ^ | | | + | v v v + User actions Session state store Token/role policy Meeting callbacks +``` + +## Why this split + +- Keeps SDK secret server-side. +- Prevents UI from owning auth/security logic. +- Enables deterministic retry policy on join/start failures. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..65cb2393 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# Android Lifecycle Workflow + +## Core Sequence + +1. App boot + permissions precheck (camera, mic, notifications as needed). +2. SDK initialize (`InitSDK`-style flow in Android layer). +3. Auth for SDK access (non-login/API user paths or signed flow). +4. Join/start meeting request. +5. In-meeting event handling (audio/video/chat/share/BO/recording where enabled). +6. Leave/end meeting and release/cleanup. + +## Failure Domains + +- Initialization errors (SDK state, app config, package conflicts). +- Auth/signature mismatches (expired token, role mismatch, malformed payload). +- Join/start parameter mismatch (meeting number, passcode, meeting type). +- Device/media state mismatch (permissions, audio route, camera availability). diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/examples/join-start-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/examples/join-start-pattern.md new file mode 100644 index 00000000..12584d6a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# Android Join/Start Pattern + +## Join (attendee) + +1. Backend creates short-lived SDK signature/JWT. +2. App initializes SDK and verifies init callback success. +3. App executes join with normalized meeting number + passcode. +4. App subscribes to meeting status callbacks before join call returns. + +## Start (host) + +1. Backend resolves host token (`ZAK`) + role-aware signature. +2. App executes start flow and validates host privilege errors explicitly. +3. App applies host-only features conditionally (recording, management controls). + +## Guardrails + +- Do not hardcode secret in app. +- Normalize meeting identifiers as strings of digits. +- Treat SDK callback thread behavior as asynchronous; avoid UI blocking. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/references/android-reference-map.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/android-reference-map.md new file mode 100644 index 00000000..2eb77e12 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/android-reference-map.md @@ -0,0 +1,35 @@ +# Android Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/android/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/android/index.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `70` +- API reference pages captured: `1003` + +## Key API Entry Pages + +- `index.md` +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions.md` and `functions_*` +- `functions_func_*` +- `functions_vars_*` + +## Notable API Surface Areas + +- Meeting lifecycle and service controllers +- Audio/video/share controllers +- Breakout room and webinar interfaces +- AI Companion / smart summary interfaces +- Raw data helpers and delegates + +## Drift Signals to Watch + +- Newly added `AI Companion`, `smart summary`, and `avatar` interfaces. +- Legacy helper names retained with new parallel interfaces. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/references/environment-variables.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/environment-variables.md new file mode 100644 index 00000000..a3e1fecc --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/environment-variables.md @@ -0,0 +1,15 @@ +# Android Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Generate signatures server-side only. +- Keep mobile client as consumer of short-lived tokens, not secret holder. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/versioning-and-compatibility.md new file mode 100644 index 00000000..d4cb5fcc --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# Android Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.5.37500` +- Docs baseline: current Meeting SDK Android docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK artifact version in Gradle. +- Track enum/interface additions between releases. +- Validate custom UI flows after each SDK update (higher break risk vs default UI). + +## Contradiction/Drift Notes + +- Docs contain legacy and current path variants (`add-features` vs `custom-ui/default-ui` sections). +- Some page titles include suffix artifacts (for example `| MSDK | And`), indicating doc-generation inconsistencies, not functional API differences. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..c9fd7635 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# Android High-Level Scenarios + +## Scenario 1: Field-service meeting embed + +- Technician app embeds meeting in default UI. +- Uses attendee join tokens from backend. +- Adds device telemetry + service-quality diagnostics. + +## Scenario 2: Branded healthcare consult + +- Start from default UI for reliability. +- Phase in custom UI for constrained controls and guided workflows. +- Enforce strict permission and foreground-service handling. + +## Scenario 3: Training/coaching session app + +- Uses breakout room and in-meeting chat flows. +- Tracks lifecycle events for attendance and session analytics. +- Uses migration-safe handling for renamed SDK enums across upgrades. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/android/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..be0acafa --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/android/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Android Common Issues + +## 1. Init succeeds, join fails immediately + +- Verify signature freshness and role correctness. +- Confirm meeting number/passcode are exact values. +- Confirm auth path aligns with join/start path (attendee vs host). + +## 2. Works in sample, fails in app + +- Compare manifest permissions and ProGuard/R8 rules. +- Compare Gradle dependency graph for collisions. +- Confirm lifecycle ownership (Activity recreation handling). + +## 3. Custom UI instability + +- Validate default UI parity first. +- Re-check event registration ordering before rendering operations. +- Guard against null/late user stream references. + +## 4. Version drift breakage + +- Rebuild against pinned SDK version. +- Revisit renamed interfaces in API reference map. +- Re-test breakout, raw data, and advanced feature modules after upgrade. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/RUNBOOK.md new file mode 100644 index 00000000..ebd18625 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/RUNBOOK.md @@ -0,0 +1,63 @@ +# Meeting SDK Electron 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Electron (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/electron/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/electron/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/SKILL.md new file mode 100644 index 00000000..272f6b60 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/SKILL.md @@ -0,0 +1,86 @@ +--- +name: zoom-meeting-sdk-electron +description: | + Zoom Meeting SDK for Electron desktop applications. Use when embedding Zoom meetings in an Electron app + with the Node addon wrapper, JWT auth, join/start flows, settings controllers, and raw data integration. +user-invocable: false +triggers: + - electron meeting sdk + - zoom meeting sdk electron + - embed zoom in electron + - electron zoom bot + - zoom node addon + - zoom raw data electron +--- + +# Zoom Meeting SDK (Electron) + +Use this skill when building Electron desktop apps that embed Zoom Meeting SDK capabilities through the Electron wrapper. + +## Start Here + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> auth -> join/start -> in-meeting -> cleanup +2. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - service/controller/event model in Electron +3. **[Setup Guide](examples/setup-guide.md)** - dependency and build expectations +4. **[Authentication Pattern](examples/authentication-pattern.md)** - SDK JWT generation and auth callbacks +5. **[Join Meeting Pattern](examples/join-meeting-pattern.md)** - start/join meeting execution flow +6. **[SKILL.md](SKILL.md)** - full navigation + +## Core Notes + +- Electron wrapper is built on top of native Meeting SDK with Node addon bridges. +- Keep SDK key/secret server-side; generate SDK JWT on backend. +- Feature support differs by platform/version; check module docs before implementation. +- Raw data and IPC patterns require explicit security hardening in production. + +## References + +- [Electron API Reference Index](references/electron-reference.md) +- [Module Map](references/module-map.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Related Skills + +- [zoom-meeting-sdk](../SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) +- [zoom-general](../../general/SKILL.md) + + +## Merged from meeting-sdk/electron/SKILL.md + +# Zoom Meeting SDK Electron - Documentation Index + +## Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +4. [Setup Guide](examples/setup-guide.md) + +## Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +## Examples + +- [Setup Guide](examples/setup-guide.md) +- [Authentication Pattern](examples/authentication-pattern.md) +- [Join Meeting Pattern](examples/join-meeting-pattern.md) +- [Raw Data Pattern](examples/raw-data-pattern.md) + +## References + +- [Electron API Reference](references/electron-reference.md) +- [Module Map](references/module-map.md) + +## Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/high-level-scenarios.md new file mode 100644 index 00000000..e46ea3b7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/high-level-scenarios.md @@ -0,0 +1,26 @@ +# High-Level Scenarios + +## 1. Desktop internal meeting client + +- User signs in to your app. +- Backend mints SDK JWT. +- Electron app embeds join/start for scheduled meetings. +- Controllers handle mute/video/chat/share. + +## 2. Compliance-focused recorder assistant + +- Controlled join flow for operator accounts. +- Recording and raw data modules capture meeting artifacts. +- Data moves to internal compliance pipeline. + +## 3. Support operations dashboard + +- Agents join support sessions from desktop app. +- Use participants/chat/share modules for assistance workflows. +- Waiting room and host control automation for queue handling. + +## 4. AI-assisted desktop meeting copilot + +- Meeting join via Electron SDK. +- Raw data or AI-related modules feed local/remote AI services. +- Live summary/action extraction in side panel. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..957d2a01 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md @@ -0,0 +1,31 @@ +# Lifecycle Workflow + +Recommended runtime sequence for Electron Meeting SDK integrations: + +1. Initialize SDK wrapper (`zoom_sdk` level init). +2. Authenticate with SDK JWT through auth service. +3. Configure meeting parameters and settings controllers. +4. Join or start meeting through meeting service. +5. Bind meeting controllers/events (audio, video, participants, chat, share). +6. Optional raw data and advanced modules. +7. Leave meeting and release SDK resources cleanly. + +## Sequence Diagram + +```text +Electron App + -> initSDK + -> authWithJwt + -> create/get meeting service + -> joinMeeting/startMeeting + -> subscribe callbacks + -> apply controller actions + -> leaveMeeting + -> cleanup +``` + +## Why this order matters + +- Controller operations before successful auth or join usually fail or no-op. +- Settings should be applied before meeting join where possible. +- Cleanup prevents stale state and callback leaks on app relaunch. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..a3eeff38 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md @@ -0,0 +1,24 @@ +# SDK Architecture Pattern + +Electron wrapper follows a service + controller + event callback model. + +## Core layers + +- `zoom_sdk` bootstrap/auth wrappers. +- Meeting service facade. +- Feature controllers (audio, video, participants, recording, share, chat, etc.). +- Settings service/controllers. +- Optional modules (raw data, webinar, AI companion, whiteboard, QA/polling). + +## Universal pattern + +1. Get service/controller. +2. Register event callback(s). +3. Invoke async action. +4. Handle callback result/error codes. + +## Implementation guidance + +- Centralize callback routing in one internal event bus. +- Use typed wrapper methods per module to reduce invocation mistakes. +- Log SDK return codes consistently for diagnostics. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/authentication-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/authentication-pattern.md new file mode 100644 index 00000000..94c1ec95 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/authentication-pattern.md @@ -0,0 +1,20 @@ +# Authentication Pattern + +## Backend + +- Accept meeting context inputs. +- Generate short-lived Meeting SDK JWT. +- Return token to authenticated Electron client session. + +## Electron app + +1. Initialize SDK. +2. Send SDK JWT to auth module. +3. Wait for auth callback success. +4. Continue to meeting join/start. + +## Guardrails + +- Refresh token on expiry windows. +- Fail fast on auth callback errors and show actionable logs. +- Do not persist SDK secret or signing logic in Electron bundle. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/join-meeting-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/join-meeting-pattern.md new file mode 100644 index 00000000..d7cd4e61 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/join-meeting-pattern.md @@ -0,0 +1,15 @@ +# Join Meeting Pattern + +## Flow + +1. Collect meeting number, display name, passcode/credential strategy. +2. Ensure SDK auth completed. +3. Call join/start meeting API via meeting service. +4. Wait for in-meeting callbacks. +5. Initialize required controllers (audio/video/chat/share/participants). + +## Operational checks + +- Validate meeting number format before SDK call. +- Normalize role-specific fields for attendee vs host start flows. +- Apply settings defaults (audio/video/share) before join when supported. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/raw-data-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/raw-data-pattern.md new file mode 100644 index 00000000..3242ac87 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/raw-data-pattern.md @@ -0,0 +1,22 @@ +# Raw Data Pattern + +Raw data flows are advanced and require hardening. + +## Typical use cases + +- Local AI processing. +- Quality monitoring. +- Compliance capture. + +## Pattern + +1. Enable raw data module after meeting join. +2. Subscribe to relevant streams. +3. Transfer frames/samples through controlled IPC/data path. +4. Apply backpressure, buffering, and clean shutdown handling. + +## Risks + +- Performance overhead if frame handling is not bounded. +- Sensitive data exposure if raw buffers are not protected. +- Version mismatch risks in native addon dependencies. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/setup-guide.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/setup-guide.md new file mode 100644 index 00000000..ea744bba --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/examples/setup-guide.md @@ -0,0 +1,23 @@ +# Setup Guide + +This guide focuses on integration structure, not one-click install scripts. + +## Prerequisites + +- Electron + Node toolchain compatible with your chosen SDK package. +- Native build toolchain for Node addon compilation. +- Backend endpoint for SDK JWT signing. + +## Minimal setup checklist + +1. Add Meeting SDK Electron package and native artifacts. +2. Wire preload/main process APIs for SDK invocation. +3. Implement secure backend endpoint for JWT generation. +4. Add app-level init/auth/join lifecycle handlers. +5. Add structured logging around SDK callbacks and error codes. + +## Security baseline + +- Keep SDK secret off client. +- Gate any raw data transport with encryption and access controls. +- Validate all IPC boundaries between renderer and main process. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/electron-reference.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/electron-reference.md new file mode 100644 index 00000000..b2e41407 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/electron-reference.md @@ -0,0 +1,17 @@ +# Electron API Reference + +Primary crawled references: + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron.md` +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron/download-and-install.md` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/electron/index.md` + +Reference set includes module docs for: + +- auth and SDK bootstrap +- meeting service and feature controllers +- settings controllers +- raw data and advanced modules +- generated JS wrapper documentation + +Use [Module Map](module-map.md) to navigate quickly by feature area. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/module-map.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/module-map.md new file mode 100644 index 00000000..17a0b2a3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/references/module-map.md @@ -0,0 +1,33 @@ +# Module Map + +## Bootstrap and auth + +- `module-zoom_electron_sdk.md` +- `module-zoom_auth.md` +- `module-zoom_meeting.md` + +## Core meeting controls + +- `module-zoom_meeting_audio.md` +- `module-zoom_meeting_video.md` +- `module-zoom_meeting_share.md` +- `module-zoom_meeting_participants_ctrl.md` +- `module-zoom_meeting_chat.md` +- `module-zoom_meeting_recording.md` + +## Settings + +- `module-zoom_setting.md` +- `module-zoom_setting_audio.md` +- `module-zoom_setting_video.md` +- `module-zoom_setting_share.md` +- `module-zoom_setting_recording.md` + +## Advanced/optional + +- `module-zoom_rawdata.md` +- `module-zoom_meeting_webinar.md` +- `module-zoom_meeting_ai_companion.md` +- `module-zoom_meeting_whiteboard.md` +- `module-zoom_meeting_polling.md` +- `module-zoom_meeting_qa.md` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/common-issues.md new file mode 100644 index 00000000..c35b42aa --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Common Issues + +## SDK initializes but join fails + +- Verify auth callback success before join. +- Validate meeting number/passcode format. +- Confirm role and host-specific fields for start flow. + +## Native addon load failures + +- Check Electron/Node ABI compatibility. +- Rebuild native modules for your Electron version. +- Confirm platform binaries are present in package/runtime paths. + +## Callback silence or partial feature behavior + +- Ensure controller callbacks are registered before invoking actions. +- Avoid duplicate singleton initialization in multiple processes. +- Confirm feature availability for account type/meeting type. + +## Raw data instability + +- Apply bounded queues and worker separation. +- Reduce frame sampling rates if renderer/main process saturates. +- Ensure graceful unsubscription on leave/cleanup. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..4eb467ec --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,28 @@ +# Deprecated and Contradictions + +Observed from crawled docs and provided SDK package context: + +## 1. Electron version guidance contradiction + +- Package README mentions installing Electron `33.0.0`. +- Same README also says sample app currently does not support Electron 10 or above. + +Action: treat sample README version text as inconsistent and validate against official current compatibility guidance before rollout. + +## 2. Deprecated module flags in API reference + +Crawled API reference marks deprecations in several areas, including webinar and some setting-related modules. + +Action: avoid new dependencies on deprecated modules; isolate behind adapter interfaces if legacy support is required. + +## 3. Feature availability caveats + +Multiple module docs include "not supported" notes depending on platform/account/meeting context. + +Action: gate feature use with runtime capability checks and fail gracefully. + +## 4. Python/build toolchain caveat in sample notes + +Sample package notes mention build issues with newer Python (distutils-related) and suggest older Python versions. + +Action: pin build environment versions in CI and document exact toolchain used for your release branch. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/version-drift.md b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/version-drift.md new file mode 100644 index 00000000..4fe990dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/electron/troubleshooting/version-drift.md @@ -0,0 +1,17 @@ +# Version Drift + +Electron SDK integrations are sensitive to dependency drift. + +## Drift vectors + +- Electron runtime upgrades +- Node ABI changes +- Native addon compiler toolchain changes +- Meeting SDK wrapper package updates + +## Control strategy + +1. Pin tested Electron + SDK versions. +2. Keep a compatibility matrix in your project docs. +3. Rebuild and smoke test on every runtime change. +4. Run callback/error-code regression tests for join/start/audio/video/share. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/RUNBOOK.md new file mode 100644 index 00000000..72a69b49 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK iOS 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for iOS (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/ios/ +- https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/ios/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/ios/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/SKILL.md new file mode 100644 index 00000000..e34cdfd4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/SKILL.md @@ -0,0 +1,41 @@ +--- +name: zoom-meeting-sdk-ios +description: | + Zoom Meeting SDK for iOS native apps. Use when embedding Zoom meetings in iOS with + default/custom UI, PKCE + SDK auth, host start with ZAK, and mobile lifecycle handling. +user-invocable: false +triggers: + - "meeting sdk ios" + - "zoom ios sdk" + - "mobilertc ios" + - "ios default ui" + - "ios custom ui" + - "join meeting ios" + - "start meeting ios" +--- + +# Zoom Meeting SDK (iOS) + +Use this skill when building iOS apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [ios.md](ios.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/ios-reference-map.md](references/ios-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/ios/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/architecture.md new file mode 100644 index 00000000..63b8a516 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/architecture.md @@ -0,0 +1,23 @@ +# iOS Architecture + +## Layer Model + +- UIKit/SwiftUI app layer. +- Meeting orchestration layer (state machine + delegate fan-out). +- SDK adapter layer (MobileRTC service wrappers). +- Backend signature/token service. + +## Reference Flow + +```text +iOS UI -> Meeting Coordinator -> Backend Sign Service -> Meeting SDK + ^ | | | + | v v v +User intents Local state store Role/token policy SDK delegates/events +``` + +## Why this split + +- Keeps security-critical logic server-side. +- Stabilizes delegate/event ordering. +- Makes upgrade drift easier to isolate. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..04cceadb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# iOS Lifecycle Workflow + +## Core Sequence + +1. App launch and permission strategy (camera/mic, optional share path prep). +2. SDK initialization and auth callbacks. +3. Join/start decision branch. +4. In-meeting event wiring (audio/video/chat/share/webinar where needed). +5. Background/foreground transitions and interruption recovery. +6. Leave/end and cleanup. + +## Failure Domains + +- Expired or mismatched signature data. +- Role/host-token mismatch in start flow. +- App lifecycle interruption (phone call/audio route/background). +- Incomplete delegate registration before meeting transitions. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/examples/join-start-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/examples/join-start-pattern.md new file mode 100644 index 00000000..03a804c3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# iOS Join/Start Pattern + +## Join (attendee) + +1. Request short-lived signature from backend. +2. Initialize/auth SDK and verify callback success. +3. Call join with meeting number/passcode/display name. +4. Observe meeting status and user/video delegate events. + +## Start (host) + +1. Backend provides host `ZAK` + role-aware signature. +2. Call start path with host token. +3. Validate host-only feature permissions before showing controls. + +## Guardrails + +- Do not put SDK secret in iOS app bundle. +- Register delegates before initiating meeting transitions. +- Persist minimal state needed for background recovery. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/ios.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/ios.md new file mode 100644 index 00000000..8e95dd58 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/ios.md @@ -0,0 +1,18 @@ +# Meeting SDK iOS Guide + +## Scope + +iOS Meeting SDK integration for init/auth, default/custom UI, meeting join/start, and in-meeting features. + +## Validation Snapshot + +- Docs coverage includes: setup/get-started, default UI and custom UI feature tracks, PKCE/start/join/auth, FAQ/error-code pages. +- API reference snapshot includes class/protocol maps, file references, and member lists. +- Local package checked: `zoom-sdk-ios-6.7.5.33005` with `MobileRTCSample` and Objective-C sample presenters. + +## Practical Guidance + +1. Achieve stable default UI join path first. +2. Add host-start and advanced features after baseline is stable. +3. Move to custom UI only where UX requires it. +4. Add explicit permission and audio route diagnostics. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/environment-variables.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/environment-variables.md new file mode 100644 index 00000000..b58c4c2f --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/environment-variables.md @@ -0,0 +1,15 @@ +# iOS Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep secrets server-side only. +- Consider storing only short-lived meeting tokens in app memory. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/ios-reference-map.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/ios-reference-map.md new file mode 100644 index 00000000..475618f9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/ios-reference-map.md @@ -0,0 +1,33 @@ +# iOS Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/ios/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `55` +- API reference pages captured: `643` + +## Key API Entry Pages + +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions*` +- `globals*` +- `pages.md` + +## Notable API Surface Areas + +- `MobileRTCMeetingService` category extensions +- Protocol-heavy delegate architecture +- Audio/video/share/raw-data helpers +- BO/webinar/AI companion interfaces + +## Drift Signals to Watch + +- New AI Companion and smart summary handlers. +- Category-level method movement between releases. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/versioning-and-compatibility.md new file mode 100644 index 00000000..62f7bee2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# iOS Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.5.33005` +- Docs baseline: current iOS Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK package release. +- Re-verify category/protocol method availability on upgrades. +- Re-test background/audio interruption flows every upgrade. + +## Contradiction/Drift Notes + +- Package `README.md` points to generic Meeting SDK docs root, while platform docs live at `/meeting-sdk/ios/`. +- Package changelog file only links externally; keep your own integration delta notes per release. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..b843efe9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# iOS High-Level Scenarios + +## Scenario 1: Telehealth client app + +- Attendee join with strict permission prompts. +- Uses waiting-room and status callbacks for guided UX. +- Records app-level diagnostic events for support triage. + +## Scenario 2: Internal host app + +- Uses host start flow with `ZAK`. +- Enables moderator tools only after host privilege confirmation. +- Applies policy checks for recording and participant management. + +## Scenario 3: Branded custom in-meeting experience + +- Starts from default UI parity baseline. +- Adds custom UI modules for selected controls/views. +- Maintains fallback path when advanced features regress after SDK update. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/ios/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..dcaf6c86 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/ios/troubleshooting/common-issues.md @@ -0,0 +1,22 @@ +# iOS Common Issues + +## 1. Join/start fails with generic error + +- Validate signature expiry and role. +- Confirm meeting number/passcode mapping. +- Confirm host flow has valid `ZAK`. + +## 2. Delegate callbacks missing or late + +- Register delegates before invoking join/start. +- Verify lifecycle transitions do not deallocate coordinator/service objects. + +## 3. Audio route or interruption issues + +- Handle route changes explicitly. +- Re-sync meeting media state after interruption/background return. + +## 4. Upgrade regressions + +- Compare protocol/category signatures against prior version. +- Re-test custom UI extensions first; they are usually most drift-prone. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/RUNBOOK.md new file mode 100644 index 00000000..2a9d994b --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Linux 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Linux (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/linux/ +- https://marketplacefront.zoom.us/sdk/meeting/linux/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/linux/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/linux/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/SKILL.md new file mode 100644 index 00000000..a1e1256a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/SKILL.md @@ -0,0 +1,429 @@ +--- +name: meeting-sdk/linux +description: "Zoom Meeting SDK for Linux - C++ headless meeting bots with raw audio/video access, transcription, recording, and AI integration for server-side automation" +user-invocable: false +triggers: + - "linux meeting bot" + - "headless zoom bot" + - "meeting sdk linux" + - "zoom raw recording linux" + - "meeting transcription bot" + - "zoom docker bot" + - "pulseaudio zoom" + - "meeting sdk raw data" +--- + +# Zoom Meeting SDK - Linux Development + +Expert guidance for building headless meeting bots with the Zoom Meeting SDK on Linux. This SDK enables server-side meeting participation, raw media capture, transcription, and AI-powered meeting automation. + +## How to Build a Meeting Bot That Automatically Joins and Records + +Use this skill when the requirement is: +- visible bot joins a real Zoom meeting +- the bot records raw media itself +- or the bot triggers a Zoom-managed cloud-recording workflow after join + +Skill chain: +- primary: `meeting-sdk/linux` +- add `zoom-rest-api` for OBF/ZAK lookup, scheduling, or cloud-recording settings +- add `zoom-webhooks` when post-meeting cloud recording retrieval is required + +Minimal raw-recording flow: + +```cpp +JoinParam join_param; +join_param.userType = SDK_UT_WITHOUT_LOGIN; +auto& params = join_param.param.withoutloginuserJoin; +params.meetingNumber = meeting_number; +params.userName = "Recording Bot"; +params.psw = meeting_password.c_str(); +params.app_privilege_token = obf_token.c_str(); + +SDKError join_err = meeting_service->Join(join_param); +if (join_err != SDKERR_SUCCESS) { + throw std::runtime_error("join_failed"); +} + +// In MEETING_STATUS_INMEETING callback: +auto* record_ctrl = meeting_service->GetMeetingRecordingController(); +if (!record_ctrl) { + throw std::runtime_error("recording_controller_unavailable"); +} + +if (record_ctrl->CanStartRawRecording() != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); +} + +SDKError record_err = record_ctrl->StartRawRecording(); +if (record_err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); +} + +GetAudioRawdataHelper()->subscribe(new MyAudioDelegate()); +``` + +Use **raw recording** when the bot must own PCM/YUV media or feed an AI pipeline directly. +Use **cloud recording + webhooks** when the requirement is Zoom-managed MP4/M4A/transcript assets after the meeting. + +**Official Documentation**: https://developers.zoom.us/docs/meeting-sdk/linux/ +**API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +**Sample Repository (Raw Recording)**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +**Sample Repository (Headless)**: https://github.com/zoom/meetingsdk-headless-linux-sample + +## Quick Links + +**New to Meeting SDK Linux? Follow this path:** + +1. **[linux.md](linux.md)** - Quick start guide with complete workflow +2. **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures +3. **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot with retry logic +4. **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, Docker, CMake + +**Common Use Cases:** +- **Transcription Bot** → [high-level-scenarios.md#scenario-1-transcription-bot](concepts/high-level-scenarios.md) +- **Recording Bot** → [high-level-scenarios.md#scenario-2-recording-bot](concepts/high-level-scenarios.md) +- **AI Meeting Assistant** → [high-level-scenarios.md#scenario-3-ai-meeting-assistant](concepts/high-level-scenarios.md) +- **Quality Monitoring** → [high-level-scenarios.md#scenario-4-monitoring-quality-bot](concepts/high-level-scenarios.md) +- **Auto-Join + Recording Bot** → [meeting-sdk-bot.md](meeting-sdk-bot.md) for raw recording orchestration, retry, and cloud-recording handoff + +**Having issues?** +- Docker audio issues → [references/linux-reference.md#pulseaudio-setup](references/linux-reference.md) +- Raw recording permission denied → [meeting-sdk-bot.md#raw-recording-permission-denied](meeting-sdk-bot.md) +- Build errors → [references/linux-reference.md#troubleshooting](references/linux-reference.md) + +## Routing Rule for Bots + +If the user asks to build a bot that **automatically joins a Zoom meeting and records it**, start with +[meeting-sdk-bot.md](meeting-sdk-bot.md). + +- Use **Meeting SDK Linux** for the visible participant, join flow, and raw recording control. +- Chain **zoom-rest-api** when the bot must fetch OBF/ZAK tokens, schedule meetings, or enable account-side recording settings. +- Chain **zoom-webhooks** when the requirement is Zoom cloud recording retrieval after meeting end. + +## SDK Overview + +The Zoom Meeting SDK for Linux is a **C++ library optimized for headless server environments**: +- **Headless Operation**: No GUI required, perfect for Docker/cloud +- **Raw Data Access**: YUV420 video, PCM audio at 32kHz +- **GLib Event Loop**: Async event handling for callbacks +- **Docker-Ready**: Pre-configured Dockerfiles for CentOS/Ubuntu +- **PulseAudio Integration**: Virtual audio devices for headless environments + +### Key Differences from Video SDK + +| Feature | Meeting SDK (Linux) | Video SDK | +|---------|-------------------|-----------| +| **Primary Use** | Join existing meetings as bot | Host custom video sessions | +| **Visibility** | Visible participant | Session participant | +| **UI** | Headless (no UI) | Optional custom UI | +| **Authentication** | JWT + OBF/ZAK for external meetings | JWT only | +| **Recording Control** | `StartRawRecording()` required | Direct raw data access | +| **Platform** | Linux only | Windows, macOS, iOS, Android | + +## Prerequisites + +### System Requirements +- **OS**: Ubuntu 22+, CentOS 8/9, Oracle Linux 8 +- **Architecture**: x86_64 +- **Compiler**: gcc/g++ with C++11 support +- **Build Tools**: cmake 3.16+ + +### Development Dependencies +```bash +# Ubuntu +apt-get install -y build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libglib2.0-dev libcurl4-openssl-dev pulseaudio + +# CentOS +yum install -y cmake gcc gcc-c++ \ + libxcb-devel xcb-util-image xcb-util-keysyms \ + glib2-devel libcurl-devel pulseaudio +``` + +### Required Credentials +1. **Zoom Meeting SDK App** (Client ID & Secret) → [Create at Marketplace](https://marketplace.zoom.us/) +2. **JWT Token** → Generate from Client ID/Secret +3. **For External Meetings**: OBF token OR ZAK token → [Get via REST API](../references/bot-authentication.md) +4. **For Raw Recording**: Meeting Recording Token (optional) → [Get via API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken) + +## Quick Start + +### 1. Download & Extract SDK + +```bash +# Download from https://marketplace.zoom.us/ +tar xzf zoom-meeting-sdk-linux_x86_64-{version}.tar + +# Organize files +mkdir -p demo/include/h demo/lib/zoom_meeting_sdk +cp -r h/* demo/include/h/ +cp lib*.so demo/lib/zoom_meeting_sdk/ +cp -r qt_libs demo/lib/zoom_meeting_sdk/ +cp translation.json demo/lib/zoom_meeting_sdk/json/ + +# Create required symlink +cd demo/lib/zoom_meeting_sdk && ln -s libmeetingsdk.so libmeetingsdk.so.1 +``` + +### 2. Initialize & Auth + +```cpp +#include "zoom_sdk.h" + +USING_ZOOM_SDK_NAMESPACE + +// Initialize SDK +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +InitSDK(init_params); + +// Authenticate with JWT +AuthContext auth_ctx; +auth_ctx.jwt_token = your_jwt_token; +CreateAuthService(&auth_service); +auth_service->SDKAuth(auth_ctx); +``` + +### 3. Join Meeting + +```cpp +// In onAuthenticationReturn callback +void onAuthenticationReturn(AuthResult ret) { + if (ret == AUTHRET_SUCCESS) { + JoinParam join_param; + join_param.userType = SDK_UT_WITHOUT_LOGIN; + + auto& params = join_param.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.userName = "Bot"; + params.psw = "password"; + params.isVideoOff = true; + params.isAudioOff = false; + + meeting_service->Join(join_param); + } +} +``` + +### 4. Access Raw Data + +```cpp +// In onMeetingStatusChanged callback +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_INMEETING) { + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Start raw recording (enables raw data access) + if (record_ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + audio_helper->subscribe(new MyAudioDelegate()); + + // Subscribe to video + IZoomSDKRenderer* video_renderer; + createRenderer(&video_renderer, new MyVideoDelegate()); + video_renderer->setRawDataResolution(ZoomSDKResolution_720P); + video_renderer->subscribe(user_id, RAW_DATA_TYPE_VIDEO); + } + } +} +``` + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Headless Operation** | No GUI, perfect for Docker/server deployments | +| **Raw Audio (PCM)** | Capture mixed or per-user audio at 32kHz | +| **Raw Video (YUV420)** | Capture video frames in contiguous planar format | +| **GLib Event Loop** | Async callback handling | +| **Docker Support** | Pre-built Dockerfiles for CentOS/Ubuntu | +| **PulseAudio Virtual Devices** | Audio in headless environments | +| **Breakout Rooms** | Programmatic breakout room management | +| **Chat** | Send/receive in-meeting chat | +| **Recording Control** | Local, cloud, and raw recording | + +## Critical Gotchas & Best Practices + +### ⚠️ CRITICAL: PulseAudio for Docker/Headless + +**The #1 issue for raw audio in Docker:** + +Raw audio requires PulseAudio and a config file, even in headless environments. + +**Solution**: +```bash +# Install PulseAudio +apt-get install -y pulseaudio pulseaudio-utils + +# Create config file +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << EOF +[General] +system.audio.type=default +EOF + +# Start PulseAudio with virtual devices +pulseaudio --start --exit-idle-time=-1 +pactl load-module module-null-sink sink_name=virtual_speaker +pactl load-module module-null-sink sink_name=virtual_mic +``` + +See: [references/linux-reference.md#pulseaudio-setup](references/linux-reference.md) + +### Raw Recording Permission Required + +Unlike Video SDK, Meeting SDK requires explicit permission to access raw data: + +```cpp +// MUST call StartRawRecording() first +auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + +SDKError can_record = record_ctrl->CanStartRawRecording(); +if (can_record == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + // NOW you can subscribe to audio/video +} else { + // Need: host/co-host status OR recording token +} +``` + +**Ways to get permission**: +1. Bot is host/co-host +2. Host grants recording permission +3. Use `recording_token` parameter (get via [REST API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken)) +4. Use `app_privilege_token` (OBF) when joining + +### GLib Main Loop Required + +SDK callbacks execute via GLib event loop: + +```cpp +#include + +// Setup main loop +GMainLoop* loop = g_main_loop_new(NULL, FALSE); + +// Add timeout for periodic tasks +g_timeout_add_seconds(10, check_meeting_status, NULL); + +// Run loop (blocks until quit) +g_main_loop_run(loop); +``` + +**Without GLib loop**: Callbacks never fire, join hangs indefinitely. + +### Heap Memory Mode Recommended + +Always use heap mode for raw data to avoid stack overflow with large frames: + +```cpp +init_params.rawdataOpts.videoRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +init_params.rawdataOpts.shareRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +``` + +### Thread Safety + +SDK callbacks execute on SDK threads: +- Don't perform heavy operations in callbacks +- Don't call `CleanUPSDK()` from within callbacks +- Use thread-safe queues for data passing +- Use mutexes for shared state + +## Production Architectures + +### Transcription Bot + +**See**: [concepts/high-level-scenarios.md#scenario-1](concepts/high-level-scenarios.md) + +``` +Join Meeting → StartRawRecording → Subscribe Audio → +Stream to AssemblyAI/Whisper → Generate Real-time Transcript +``` + +### Recording Bot + +**See**: [concepts/high-level-scenarios.md#scenario-2](concepts/high-level-scenarios.md) + +``` +Join Meeting → StartRawRecording → Subscribe Audio+Video → +Write YUV+PCM → FFmpeg Merge → Upload to Storage +``` + +### AI Meeting Assistant + +**See**: [concepts/high-level-scenarios.md#scenario-3](concepts/high-level-scenarios.md) + +``` +Join Meeting → Real-time Transcription → AI Analysis → +Extract Action Items → Generate Summary +``` + +## Sample Applications + +**Official Repositories**: + +| Sample | Description | Link | +|--------|-------------|------| +| **Raw Recording Sample** | Traditional C++ approach with config.txt | [GitHub](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | +| **Headless Sample** | Modern TOML-based with CLI, Docker Compose | [GitHub](https://github.com/zoom/meetingsdk-headless-linux-sample) | + +**What Each Demonstrates**: + +- **Raw Recording Sample**: Complete raw data workflow, PulseAudio setup, Docker multi-distro support +- **Headless Sample**: AssemblyAI integration, Anthropic AI, production-ready config management + +## Documentation Library + +### Core Concepts +- **[linux.md](linux.md)** - Quick start & core workflow +- **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures +- **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot with retry logic + +### Platform Reference +- **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, CMake, Docker, troubleshooting + +### Authentication +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### Advanced Features +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout rooms +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| **No audio in Docker** | Missing PulseAudio config | Create `~/.config/zoomus.conf` | +| **Raw recording denied** | No permission | Use host/co-host OR recording token | +| **Callbacks not firing** | Missing GLib main loop | Add `g_main_loop_run()` | +| **Build errors (XCB)** | Missing X11 libraries | Install libxcb packages | +| **Segfault on auth** | OpenSSL version mismatch | Install libssl1.1 | + +**Complete troubleshooting**: [references/linux-reference.md#troubleshooting](references/linux-reference.md) + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +- **Dev Forum**: https://devforum.zoom.us/ +- **GitHub Samples**: + - https://github.com/zoom/meetingsdk-linux-raw-recording-sample + - https://github.com/zoom/meetingsdk-headless-linux-sample + +--- + +**Need help?** Start with [linux.md](linux.md) for quick start, then explore [high-level-scenarios.md](concepts/high-level-scenarios.md) for production patterns. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/concepts/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/concepts/high-level-scenarios.md new file mode 100644 index 00000000..123471fa --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/concepts/high-level-scenarios.md @@ -0,0 +1,406 @@ +# High-Level Scenarios for Meeting SDK Linux + +This guide covers common bot architectures and patterns that remain stable across SDK versions, with flexible approaches to handle API changes. + +## Scenario 1: Transcription Bot + +**Goal**: Join meetings to transcribe conversations in real-time. + +### Architecture + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ +│ Meeting │───►│ Meeting SDK │───►│ Audio Stream │───►│ Transcription│ +│ Starts │ │ Join + Auth │ │ (PCM 32kHz) │ │ Service │ +└─────────────┘ └──────────────┘ └─────────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ + │ StartRaw │ │ onMixedAudio │ │ Store │ + │ Recording │ │ RawDataReceived │ │ Transcript │ + └──────────────┘ └─────────────────┘ └──────────────┘ +``` + +### Core Pattern (Version-Flexible) + +```cpp +// STEP 1: Initialize (stable across versions) +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +InitSDK(init_params); + +// STEP 2: Authenticate (JWT token - stable) +AuthContext auth_ctx; +auth_ctx.jwt_token = getJWTToken(); // Generate from SDK credentials +CreateAuthService(&auth_service); +auth_service->SDKAuth(auth_ctx); + +// STEP 3: Join meeting (flexible - check SDK version for exact field names) +JoinParam join_param; +join_param.userType = SDK_UT_WITHOUT_LOGIN; +auto& params = join_param.param.withoutloginuserJoin; +params.meetingNumber = meeting_number; +params.userName = "Transcription Bot"; +params.psw = meeting_password; + +// Version-flexible: Check if these fields exist in your SDK version +// params.app_privilege_token = obf_token; // v5.15+ +// params.onBehalfToken = obf_token; // Some versions +// params.userZAK = zak_token; // Alternative + +meeting_service->Join(join_param); + +// STEP 4: In onMeetingStatusChanged callback +if (status == MEETING_STATUS_INMEETING) { + // Get recording controller (pattern stable) + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Start raw recording (enables raw data access) + if (record_ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + } + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + audio_helper->subscribe(new TranscriptionAudioDelegate()); +} + +// STEP 5: Process audio +class TranscriptionAudioDelegate : public IZoomSDKAudioRawDataDelegate { + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Send PCM data to transcription service + sendToTranscriptionAPI(data->GetBuffer(), data->GetBufferLen()); + } +}; +``` + +### Version Handling Strategy + +**Flexible field access**: +```cpp +// Define a macro to check field existence at compile time +#ifdef HAS_APP_PRIVILEGE_TOKEN + params.app_privilege_token = token; +#elif defined(HAS_ON_BEHALF_TOKEN) + params.onBehalfToken = token; +#else + params.userZAK = token; +#endif +``` + +**Runtime capability detection**: +```cpp +SDKError canRecord = record_ctrl->CanStartRawRecording(); +if (canRecord != SDKERR_SUCCESS) { + // Fallback: Try local recording + if (record_ctrl->CanStartRecording(true) == SDKERR_SUCCESS) { + record_ctrl->StartRecording(time, path); + } +} +``` + +### Key Resilience Patterns + +1. **Always check CanXXX() before calling XXX()** +2. **Have fallback authentication methods** (OBF → ZAK → password-only) +3. **Detect capabilities**, don't assume +4. **Use error codes** to adapt behavior + +--- + +## Scenario 2: Recording Bot (Video + Audio) + +**Goal**: Record meetings with synchronized audio and video. + +### Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ +│ Join Meeting │───►│ StartRawRecording│───►│ Subscribe │ +│ │ │ │ │ Audio+Video │ +└──────────────┘ └─────────────────┘ └──────────────┘ + │ + ┌──────────────────────────┴─────────────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ Write YUV420 │ │ Write PCM │ + │ video.yuv │ │ audio.pcm │ + └──────────────┘ └──────────────┘ + │ │ + └──────────────────────────────────────────┬─┘ + ▼ + ┌──────────────┐ + │ FFmpeg Merge │ + │ output.mp4 │ + └──────────────┘ +``` + +### Core Pattern + +```cpp +// After joining and starting raw recording... + +// Video subscription +class VideoRecorderDelegate : public IZoomSDKRendererDelegate { + std::ofstream videoFile; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // YUV420 format: Y + U + V planes (contiguous) + videoFile.write(data->GetYBuffer(), width * height); + videoFile.write(data->GetUBuffer(), (width/2) * (height/2)); + videoFile.write(data->GetVBuffer(), (width/2) * (height/2)); + } +}; + +IZoomSDKRenderer* video_renderer; +createRenderer(&video_renderer, new VideoRecorderDelegate()); +video_renderer->setRawDataResolution(ZoomSDKResolution_720P); +video_renderer->subscribe(user_id, RAW_DATA_TYPE_VIDEO); + +// Audio subscription +class AudioRecorderDelegate : public IZoomSDKAudioRawDataDelegate { + std::ofstream audioFile; + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + audioFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + } +}; + +auto* audio_helper = GetAudioRawdataHelper(); +audio_helper->subscribe(new AudioRecorderDelegate()); + +// Post-processing with FFmpeg +system("ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i video.yuv " + "-f s16le -ar 32000 -ac 1 -i audio.pcm " + "-c:v libx264 -c:a aac -shortest output.mp4"); +``` + +### Handling Resolution Changes + +**Flexible resolution selection** (adapt to SDK version): +```cpp +// Try highest resolution available, fall back gracefully +ZoomSDKResolution resolutions[] = { + ZoomSDKResolution_1080P, + ZoomSDKResolution_720P, + ZoomSDKResolution_360P +}; + +for (auto res : resolutions) { + SDKError err = video_renderer->setRawDataResolution(res); + if (err == SDKERR_SUCCESS) { + std::cout << "Using resolution: " << res << std::endl; + break; + } +} +``` + +--- + +## Scenario 3: AI Meeting Assistant + +**Goal**: Real-time meeting analysis with AI (sentiment, action items, summaries). + +### Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Audio Stream │───►│ Transcription │───►│ AI Analysis │───►│ Actions │ +│ (Real-time) │ │ (AssemblyAI) │ │ (Anthropic) │ │ Identified │ +└──────────────┘ └─────────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ + │ Speaker │ │ Sentiment │ │ Generate │ + │ Diarization │ │ Analysis │ │ Summary │ + └─────────────────┘ └─────────────────┘ └──────────────┘ +``` + +### Core Pattern + +```cpp +// Real-time streaming transcription +class AIAssistantAudioDelegate : public IZoomSDKAudioRawDataDelegate { + WebSocketClient transcription_ws; // AssemblyAI WebSocket + AIClient ai_client; // Anthropic/OpenAI + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Stream audio to real-time transcription + transcription_ws.send_audio(data->GetBuffer(), data->GetBufferLen()); + } +}; + +// Process transcription results +void onTranscriptReceived(const std::string& text, const std::string& speaker) { + // Accumulate context + meeting_context += speaker + ": " + text + "\n"; + + // Every 30 seconds, analyze with AI + if (should_analyze()) { + std::string analysis = ai_client.analyze(meeting_context, { + "extract_action_items", + "detect_decisions", + "identify_blockers" + }); + + // Store results + meeting_insights.push_back(analysis); + } +} + +// Post-meeting summary +void onMeetingEnded() { + std::string summary = ai_client.summarize(meeting_context, meeting_insights); + save_to_database(meeting_id, summary); + send_email_summary(participants, summary); +} +``` + +### Version Flexibility: Chat Integration + +Some SDK versions support in-meeting chat: +```cpp +// Try to send AI insights to meeting chat +auto* chat_ctrl = meeting_service->GetMeetingChatController(); +if (chat_ctrl) { + // Check if available + if (chat_ctrl->CanSendChat()) { + chat_ctrl->SendChatTo("AI detected action item: ..."); + } +} +``` + +--- + +## Scenario 4: Monitoring & Quality Bot + +**Goal**: Monitor meeting quality metrics and alert on issues. + +### Core Pattern + +```cpp +// Get service quality info +auto* meeting_info = meeting_service->GetMeetingInfo(); + +// Polling loop (GLib timeout) +gboolean check_quality(gpointer data) { + // Audio stats + auto* audio_stats = meeting_info->GetAudioStatistics(); + if (audio_stats) { + int jitter = audio_stats->jitter; + int packet_loss = audio_stats->packet_loss_percent; + + if (packet_loss > 5) { + alert("High packet loss: " + std::to_string(packet_loss) + "%"); + } + } + + // Video stats + auto* video_stats = meeting_info->GetVideoStatistics(); + if (video_stats) { + int fps = video_stats->fps; + int resolution_width = video_stats->width; + + if (fps < 15) { + alert("Low FPS: " + std::to_string(fps)); + } + } + + return TRUE; // Continue polling +} + +// Setup polling +g_timeout_add_seconds(10, check_quality, nullptr); +``` + +--- + +## Version Migration Guide + +### When SDK Updates + +1. **Read release notes** for breaking changes +2. **Check header files** for actual method signatures +3. **Test compilation** with `-Werror=deprecated` +4. **Update authentication** if new token types added +5. **Verify raw data flow** still works + +### Common Breaking Changes + +| Change Type | Example | Mitigation | +|-------------|---------|------------| +| Struct field rename | `withoutloginuserJoin` → `withoutLoginUserJoin` | Use `#ifdef` or update | +| Method signature change | `subscribe(user_id)` → `subscribe(user_id, type)` | Check return codes | +| Enum value rename | `SDKERR_NORECORDINGINPROCESS` → `SDKERR_NO_RECORDING_IN_PROCESS` | Map old → new | +| New required field | `app_privilege_token` becomes mandatory | Add to config | + +### Testing Strategy + +```cpp +// Version detection at compile time +#if ZOOM_SDK_VERSION >= 51500 // v5.15.0 + #define USE_OBF_TOKEN +#endif + +// Runtime capability testing +bool test_raw_recording() { + auto* ctrl = meeting_service->GetMeetingRecordingController(); + return ctrl && ctrl->CanStartRawRecording() == SDKERR_SUCCESS; +} +``` + +--- + +## Docker Deployment Patterns + +### Multi-Stage Build (Version-Flexible) + +```dockerfile +FROM ubuntu:22.04 AS builder + +# Install dependencies (stable) +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 \ + libglib2.0-dev libcurl4-openssl-dev + +# Copy SDK (any version) +COPY zoom-meeting-sdk-linux_*.tar.gz /tmp/ +RUN cd /tmp && tar xzf zoom-meeting-sdk-linux_*.tar.gz + +# Build app +COPY . /app +WORKDIR /app +RUN cmake -B build && cd build && make + +# Runtime stage +FROM ubuntu:22.04 +COPY --from=builder /app/build/meetingBot /usr/local/bin/ +COPY --from=builder /tmp/lib*.so /usr/local/lib/ + +# PulseAudio setup (required for raw audio) +RUN apt-get update && apt-get install -y pulseaudio && \ + mkdir -p ~/.config && \ + echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf + +CMD ["meetingBot"] +``` + +--- + +## Summary: Resilient Bot Checklist + +✅ **Always check capabilities** before calling methods +✅ **Use heap memory mode** for raw data +✅ **Handle authentication fallbacks** (OBF → ZAK → password) +✅ **Detect SDK version** at compile/runtime +✅ **Test with actual SDK headers**, not documentation examples +✅ **Setup PulseAudio** for Docker/headless environments +✅ **Parse error codes** to adapt behavior +✅ **Keep authentication tokens fresh** (regenerate before expiry) +✅ **Log everything** for debugging version-specific issues diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/linux.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/linux.md new file mode 100644 index 00000000..34cc9e9a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/linux.md @@ -0,0 +1,363 @@ +# Zoom Meeting SDK (Linux) + +Embed Zoom meeting capabilities into Linux applications for headless meeting bots and server-side integrations. + +## Prerequisites + +- Zoom app with Meeting SDK credentials (Client ID & Secret) +- Docker (recommended) or Linux environment (Ubuntu 22+, CentOS 8/9) +- C++ development toolchain (cmake, gcc, g++) +- GLib 2.0, libcurl, pthread + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT token generation. + +## Overview + +The Linux SDK is a **C++ native SDK** designed for: +- **Headless meeting bots** - Join meetings without UI +- **Raw media access** - Capture/send audio/video streams +- **Server-side automation** - Scalable meeting integrations + +## Quick Start + +### 1. Download Linux SDK + +Download from [Zoom Marketplace](https://marketplace.zoom.us/): +- Extract `zoom-meeting-sdk-linux_x86_64-{version}.tar` + +### 2. Setup Project Structure + +``` +your-project/ + demo/ + include/h/ # SDK headers + lib/zoom_meeting_sdk/ + libmeetingsdk.so + libmeetingsdk.so.1 # symlink + qt_libs/ + json/translations.json + meeting_sdk_demo.cpp + CMakeLists.txt + config.txt +``` + +Copy SDK files: +```bash +cp -r h/* demo/include/h/ +cp lib*.so demo/lib/zoom_meeting_sdk/ +cp -r qt_libs demo/lib/zoom_meeting_sdk/ +cp translation.json demo/lib/zoom_meeting_sdk/json/ + +# Create required symlink +cd demo/lib/zoom_meeting_sdk/ +ln -s libmeetingsdk.so libmeetingsdk.so.1 +``` + +### 3. Configure Credentials + +Create `config.txt`: +``` +meeting_number: "1234567890" +token: "YOUR_JWT_TOKEN" +meeting_password: "password123" +recording_token: "" +GetVideoRawData: "true" +GetAudioRawData: "true" +SendVideoRawData: "false" +SendAudioRawData: "false" +``` + +### 4. Build & Run + +```bash +cd demo +cmake -B build +cd build +make +cd ../bin +./meetingSDKDemo +``` + +## Core Workflow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AuthSDK │───►│ JoinMeeting │───►│ Raw Data │ +│ │ │ (JWT) │ │ │ │ Subscribe │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + ▼ ▼ + OnAuthComplete onInMeeting + callback callback +``` + +## Code Examples + +### 1. Initialize SDK + +```cpp +#include "zoom_sdk.h" + +USING_ZOOM_SDK_NAMESPACE + +void InitMeetingSDK() { + SDKError err(SDKERR_SUCCESS); + InitParam initParam; + + initParam.strWebDomain = "https://zoom.us"; + initParam.strSupportUrl = "https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "Init meetingSdk:error" << std::endl; + } +} +``` + +### 2. Authenticate with JWT + +```cpp +#include "auth_service_interface.h" + +IAuthService* m_pAuthService; + +void OnAuthenticationComplete() { + JoinMeeting(); // Called on successful auth +} + +void AuthMeetingSDK() { + CreateAuthService(&m_pAuthService); + + m_pAuthService->SetEvent( + new AuthServiceEventListener(&OnAuthenticationComplete) + ); + + AuthContext param; + param.jwt_token = token.c_str(); // Your JWT token + m_pAuthService->SDKAuth(param); +} +``` + +### 3. Join Meeting + +```cpp +#include "meeting_service_interface.h" + +IMeetingService* m_pMeetingService; +ISettingService* m_pSettingService; + +void JoinMeeting() { + CreateMeetingService(&m_pMeetingService); + CreateSettingService(&m_pSettingService); + + // Set event listeners + m_pMeetingService->SetEvent( + new MeetingServiceEventListener(&onMeetingJoined, &onMeetingEnds, &onInMeeting) + ); + + // Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = std::stoull(meeting_number); + params.userName = "BotUser"; + params.psw = meeting_password.c_str(); + params.isVideoOff = false; + params.isAudioOff = false; + + m_pMeetingService->Join(joinParam); +} +``` + +### 4. Subscribe to Raw Video + +```cpp +#include "rawdata/rawdata_renderer_interface.h" +#include "rawdata/zoom_rawdata_api.h" + +class ZoomSDKRenderer : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // YUV420 (I420) format - contiguous planar data (no strides) + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Y plane: width * height bytes + outputFile.write(data->GetYBuffer(), width * height); + // U plane: (width/2) * (height/2) bytes + outputFile.write(data->GetUBuffer(), (width / 2) * (height / 2)); + // V plane: (width/2) * (height/2) bytes + outputFile.write(data->GetVBuffer(), (width / 2) * (height / 2)); + } + + void onRawDataStatusChanged(RawDataStatus status) override {} + void onRendererBeDestroyed() override {} +}; + +// Subscribe after joining +IZoomSDKRenderer* videoHelper; +ZoomSDKRenderer* videoSource = new ZoomSDKRenderer(); + +createRenderer(&videoHelper, videoSource); +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +videoHelper->subscribe(userID, RAW_DATA_TYPE_VIDEO); +``` + +### 5. Subscribe to Raw Audio + +```cpp +#include "rawdata/rawdata_audio_helper_interface.h" + +class ZoomSDKAudioRawData : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Process PCM audio (mixed from all participants) + pcmFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Process audio from specific participant + } +}; + +// Subscribe after joining +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +audioHelper->subscribe(new ZoomSDKAudioRawData()); +``` + +### 6. GLib Main Loop + +```cpp +#include + +GMainLoop* loop; + +gboolean timeout_callback(gpointer data) { + return TRUE; // Keep running +} + +int main(int argc, char* argv[]) { + InitMeetingSDK(); + AuthMeetingSDK(); + + loop = g_main_loop_new(NULL, FALSE); + g_timeout_add(1000, timeout_callback, loop); + g_main_loop_run(loop); + + return 0; +} +``` + +## Available Examples + +| Example | Description | +|---------|-------------| +| **SkeletonExample** | Minimal join meeting - start here | +| **GetRawVideoAndAudioExample** | Subscribe to raw audio/video streams | +| **GetRawVideoAndAudioAPIExample** | API-based raw data access | +| **SendRawVideoAndAudioExample** | Send custom video/audio as virtual camera/mic | +| **ChatExample** | In-meeting chat functionality | +| **BreakoutExample** | Breakout room management | +| **AllInOneExample** | Complete demo with all features | +| **SendRawVideoAndAudioWithRTMSExample** | Raw data with RTMS integration | + +## Detailed References + +### High-Level Guides +- **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures (transcription, recording, AI assistant) +- **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot pattern with retry logic + +### Platform Guide +- **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, Docker setup, troubleshooting + +### Authentication +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### Features +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout room management +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | Official headless bot with Docker | +| [meetingsdk-linux-raw-recording-sample](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | Raw audio/video access | + +## Playing Raw Video/Audio Files + +Raw YUV/PCM files have no headers - you must specify format explicitly. + +### Play Raw YUV Video +```bash +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv +``` + +### Convert YUV to MP4 +```bash +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 +``` + +### Play Raw PCM Audio +```bash +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert PCM to WAV +```bash +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### Combine Video + Audio +```bash +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +**Key flags:** +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (check output filename) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate (Zoom uses 32kHz) | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +## Compilation Tips + +> **Note**: These are general patterns - specific methods/types may vary by SDK version. + +### Event Listener Interfaces +SDK event listener interfaces (`IMeetingServiceEvent`, `IMeetingParticipantsCtrlEvent`, etc.) have **many pure virtual methods**. You must implement ALL of them, even with empty bodies, or you'll get "invalid new-expression of abstract class type" errors. Always check the SDK headers for the complete list. + +### Include Order Issues +Some SDK headers don't include their own dependencies. If you encounter undefined type errors (like `AudioType` or `time_t`), try: +- Adding standard library includes (``, ``) before SDK headers +- Including related SDK headers in dependency order +- Checking which header defines the missing type + +### Method Signature Mismatches +Reference samples may have outdated method names. Always verify against the actual SDK header files - they are the authoritative source. + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Use one of: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token` in JoinParam) +- **ZAK Token** - Zoom Access Key (`userZAK` in JoinParam) +- **On Behalf Token** - For specific use cases (`onBehalfToken` in JoinParam) + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/index.html +- **Developer forum**: https://devforum.zoom.us/ +- **SDK download**: https://marketplace.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/meeting-sdk-bot.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/meeting-sdk-bot.md new file mode 100644 index 00000000..fed4aa83 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/meeting-sdk-bot.md @@ -0,0 +1,825 @@ +# Meeting SDK Bot (Linux) + +Build resilient meeting bots that join Zoom meetings as visible participants using the Meeting SDK. + +## Overview + +Meeting SDK bots join as real participants (visible in participant list) and can access raw audio/video data for recording, transcription, or AI processing. + +**Use this approach when:** +- You need to be visible in the participant list +- You want to interact with meeting features (chat, reactions, etc.) +- You need local recording control +- You're processing your own meetings or have user consent + +**Alternative:** See [rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) for invisible, read-only access via RTMS. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ MEETING SDK BOT FLOW │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. Pre-Join: REST API │ +│ └── Get meeting schedule (number, password, start time) │ +│ └── Get OBF token for user (bot joins "on behalf of" user) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 2. Join with Retry (OBF requires owner present) │ +│ └── Retry with configurable interval until owner joins │ +│ └── Circuit breaker: Stop after N attempts │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 3. Start Raw Recording (Meeting SDK singleton) │ +│ └── IMeetingRecordingController::StartRawRecording() │ +│ └── Subscribe to raw audio/video │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 4. Process Media Streams │ +│ └── Audio: PCM data via IZoomSDKAudioRawDataDelegate │ +│ └── Video: YUV420 frames via IZoomSDKVideoRawDataDelegate │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 5. Mid-Meeting: Connection Monitoring │ +│ └── Detect disconnections → Exponential backoff retry │ +│ └── Stop after N reconnection attempts │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Skills Required + +| Skill | Purpose | +|-------|---------| +| **zoom-rest-api** | Get meeting schedule, retrieve OBF token | +| **zoom-meeting-sdk** (Linux) | Join meeting, control recording, access raw media | + +## Choose the Recording Mode + +| Goal | Primary path | Skills | +|------|--------------|--------| +| Bot writes its own audio/video files | `StartRawRecording()` + raw audio/video delegates | `zoom-meeting-sdk` (Linux) | +| Zoom-hosted MP4/M4A/transcript assets after meeting end | Meeting/account cloud recording settings + `recording.completed` webhook + recordings download API | `zoom-rest-api` + `zoom-webhooks` | + +Use raw recording when the bot must process or persist media itself. Use the cloud-recording path when the requirement is post-meeting retrieval of Zoom-managed recording assets. + +## Automatic Join + Recording Flow + +```text +zoom-rest-api + -> get meeting metadata + -> get OBF or ZAK token +Meeting SDK Linux bot + -> join with retry + -> CanStartRawRecording() + -> StartRawRecording() + -> subscribe raw audio/video + -> write PCM/YUV or forward to AI pipeline +Optional post-meeting cloud path + -> zoom-webhooks recording.completed + -> zoom-rest-api recordings download +``` + +## Prerequisites + +- Zoom Meeting SDK v5.15+ (Linux) +- Meeting SDK JWT credentials (SDK Key + Secret) +- Server-to-Server OAuth app or OAuth app for REST API access +- REST API scopes: `meeting:read`, `user:read`, optionally `meeting:write` if triggering meetings +- Raw Data entitlement enabled (Admin → Account Settings → Meeting → "Allow access to raw data") + +## Configuration + +### Retry Parameters (Customizable) + +```cpp +// config.h or environment variables +struct BotConfig { + // Join retry (waiting for owner to be present) + int join_retry_attempts = 5; // Max join attempts (default: 5) + int join_retry_interval_ms = 60000; // Constant interval: 60s (default: 1min) + + // Mid-meeting reconnection (network failures) + int reconnect_max_attempts = 3; // Max reconnection attempts (default: 3) + int reconnect_base_delay_ms = 2000; // Initial delay: 2s (default: 2s) + // Exponential backoff: 2s, 4s, 8s... + + // Meeting schedule polling (if webhook unavailable) + int schedule_poll_interval_ms = 30000; // Poll every 30s (default: 30s) + + // Timeout settings + int auth_timeout_ms = 10000; // SDK auth timeout (default: 10s) + int join_timeout_ms = 30000; // Single join attempt timeout (default: 30s) +}; + +// Load from environment variables (recommended for production) +BotConfig loadConfig() { + BotConfig cfg; + + // Override defaults from env vars if present + const char* env; + + if ((env = getenv("BOT_JOIN_RETRY_ATTEMPTS"))) { + cfg.join_retry_attempts = atoi(env); + } + if ((env = getenv("BOT_JOIN_RETRY_INTERVAL_MS"))) { + cfg.join_retry_interval_ms = atoi(env); + } + if ((env = getenv("BOT_RECONNECT_MAX_ATTEMPTS"))) { + cfg.reconnect_max_attempts = atoi(env); + } + if ((env = getenv("BOT_RECONNECT_BASE_DELAY_MS"))) { + cfg.reconnect_base_delay_ms = atoi(env); + } + + return cfg; +} +``` + +### Customization Guide + +| Parameter | Default | When to Increase | When to Decrease | +|-----------|---------|------------------|------------------| +| `join_retry_attempts` | 5 | High-priority meetings, owner often late | Testing, short-lived meetings | +| `join_retry_interval_ms` | 60000 (1min) | Meetings with long pre-join buffer | Need faster failure detection | +| `reconnect_max_attempts` | 3 | Unstable networks, critical meetings | Batch processing, cost-sensitive | +| `reconnect_base_delay_ms` | 2000 (2s) | Network latency high (international) | Local network, low latency | + +**Recommended Ranges:** +- Join retry attempts: 3-10 +- Join retry interval: 30s-5min +- Reconnect attempts: 2-5 +- Reconnect base delay: 1s-5s + +**Examples:** + +```bash +# High-priority production bot (aggressive retries) +export BOT_JOIN_RETRY_ATTEMPTS=10 +export BOT_JOIN_RETRY_INTERVAL_MS=30000 # 30s +export BOT_RECONNECT_MAX_ATTEMPTS=5 +export BOT_RECONNECT_BASE_DELAY_MS=1000 # 1s + +# Cost-sensitive batch processing (conservative) +export BOT_JOIN_RETRY_ATTEMPTS=3 +export BOT_JOIN_RETRY_INTERVAL_MS=120000 # 2min +export BOT_RECONNECT_MAX_ATTEMPTS=2 +export BOT_RECONNECT_BASE_DELAY_MS=5000 # 5s + +# Development/testing (fail fast) +export BOT_JOIN_RETRY_ATTEMPTS=2 +export BOT_JOIN_RETRY_INTERVAL_MS=10000 # 10s +export BOT_RECONNECT_MAX_ATTEMPTS=1 +export BOT_RECONNECT_BASE_DELAY_MS=1000 # 1s +``` + +## Step 1: Get Meeting Info + OBF Token (REST API) + +### Get Meeting Schedule + +```bash +# Get meeting details +curl "https://api.zoom.us/v2/meetings/{meetingId}" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +**Response:** +```json +{ + "id": 1234567890, + "topic": "Team Standup", + "start_time": "2026-02-09T15:00:00Z", + "password": "abc123", + "pmi": false +} +``` + +**Store:** meeting number, password, start time. + +### Get OBF Token + +The bot joins "on behalf of" a Zoom user. Get the user's OBF token: + +```bash +# Get OBF token for user +curl -X POST "https://api.zoom.us/v2/users/{userId}/token?type=obf&ttl=7200" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +**Response:** +```json +{ + "token": "eyJhbGc...", + "expire_in": 7200 +} +``` + +**CRITICAL:** The bot cannot join until the **owner** (the user whose OBF token you're using) is present in the meeting. This is why retry logic is essential. + +**Error if REST API fails:** ABORT. No meeting info = cannot proceed. + +## Step 2: Join Meeting with Retry Logic + +### Join Implementation + +```cpp +#include +#include + +class MeetingBot { +private: + BotConfig config; + IMeetingService* meetingService; + bool joinSuccessful = false; + bool ownerNotPresentError = false; + +public: + // Attempt to join with retry logic + bool joinMeetingWithRetry( + uint64_t meetingNumber, + const string& password, + const string& obfToken, + const string& botDisplayName + ) { + for (int attempt = 1; attempt <= config.join_retry_attempts; attempt++) { + cout << "[JOIN] Attempt " << attempt << "/" + << config.join_retry_attempts << endl; + + // Attempt join + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; + param.meetingNumber = meetingNumber; + param.userName = botDisplayName.c_str(); + param.psw = password.c_str(); + param.isVideoOff = true; // Bots typically don't send video + param.isAudioOff = false; // Need audio for transcription + param.app_privilege_token = obfToken.c_str(); // OBF token + + SDKError err = meetingService->Join(joinParam); + + if (err != SDKERR_SUCCESS) { + cerr << "[JOIN] Join() call failed: " << err << endl; + + // Check if it's a retriable error + if (shouldRetryJoin(err)) { + if (attempt < config.join_retry_attempts) { + cout << "[JOIN] Retrying in " + << (config.join_retry_interval_ms / 1000) + << "s..." << endl; + this_thread::sleep_for( + chrono::milliseconds(config.join_retry_interval_ms) + ); + continue; + } + } + + // Non-retriable error or out of attempts + cerr << "[JOIN] Giving up after " << attempt << " attempts" << endl; + return false; + } + + // Join() call succeeded, wait for callback + if (waitForJoinCallback()) { + cout << "[JOIN] Successfully joined meeting!" << endl; + return true; + } else { + cerr << "[JOIN] Join callback indicated failure" << endl; + + // If owner not present, retry + if (ownerNotPresentError && attempt < config.join_retry_attempts) { + cout << "[JOIN] Owner not present. Retrying in " + << (config.join_retry_interval_ms / 1000) << "s..." << endl; + this_thread::sleep_for( + chrono::milliseconds(config.join_retry_interval_ms) + ); + continue; + } + } + } + + cerr << "[JOIN] Failed to join after " + << config.join_retry_attempts << " attempts" << endl; + return false; + } + +private: + bool shouldRetryJoin(SDKError err) { + switch (err) { + case SDKERR_WRONG_USAGE: + case SDKERR_INVALID_PARAMETER: + case SDKERR_MODULE_LOAD_FAILED: + return false; // Configuration errors, don't retry + + case SDKERR_NETWORK_ERROR: + case SDKERR_SERVICE_FAILED: + return true; // Network/server issues, retry + + default: + return true; // Unknown error, retry to be safe + } + } + + bool waitForJoinCallback() { + // Wait for onMeetingStatusChanged callback + // Implementation depends on your callback handling + // Typical: use condition variable with timeout + + std::unique_lock lock(callbackMutex); + bool success = callbackCV.wait_for( + lock, + std::chrono::milliseconds(config.join_timeout_ms), + [this]{ return joinSuccessful || ownerNotPresentError; } + ); + + return success && joinSuccessful; + } + +public: + // Callback from SDK + void onMeetingStatusChanged(MeetingStatus status, int iResult) { + std::lock_guard lock(callbackMutex); + + if (status == MEETING_STATUS_INMEETING) { + joinSuccessful = true; + callbackCV.notify_one(); + } else if (status == MEETING_STATUS_FAILED) { + // Check error code for "owner not present" + if (iResult == MEETING_FAIL_OBF_OWNER_NOT_IN_MEETING) { + ownerNotPresentError = true; + } + joinSuccessful = false; + callbackCV.notify_one(); + } + } + +private: + std::mutex callbackMutex; + std::condition_variable callbackCV; +}; +``` + +### Common Join Errors + +| Error Code | Meaning | Action | +|------------|---------|--------| +| `MEETING_FAIL_OBF_OWNER_NOT_IN_MEETING` | OBF token owner not in meeting yet | RETRY (owner might join soon) | +| `MEETING_FAIL_MEETING_NOT_EXIST` | Meeting not started | RETRY if before end time | +| `MEETING_FAIL_INCORRECT_MEETING_NUMBER` | Wrong meeting ID | ABORT (config error) | +| `MEETING_FAIL_MEETING_NOT_START` | Meeting hasn't started | RETRY until start time | +| `MEETING_FAIL_INVALID_TOKEN` | OBF token invalid/expired | ABORT (need new token) | + +## Step 3: Start Raw Recording + +Once joined, request permission to access raw audio/video: + +```cpp +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_INMEETING) { + cout << "[BOT] Joined successfully, starting raw recording..." << endl; + + // Get recording controller + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordCtrl) { + cerr << "[BOT] Failed to get recording controller" << endl; + return; + } + + // Check permission + SDKError canRecord = recordCtrl->CanStartRawRecording(); + if (canRecord != SDKERR_SUCCESS) { + cerr << "[BOT] Cannot start raw recording: " << canRecord << endl; + cerr << "[BOT] Check: Raw Data entitlement enabled in Admin settings?" << endl; + return; + } + + // Start raw recording (enables raw data flow) + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + cerr << "[BOT] StartRawRecording failed: " << err << endl; + return; + } + + cout << "[BOT] Raw recording started, subscribing to media..." << endl; + + // Subscribe to audio/video + subscribeToRawMedia(); + } +} +``` + +**IMPORTANT:** `StartRawRecording()` does NOT create a file. It enables access to raw audio/video data streams. + +### Manage Recording Lifecycle Explicitly + +The bot should treat raw recording as a capability switch plus media subscriptions: + +```cpp +class RecordingSession { +public: + void start(IMeetingService* meetingService) { + auto* recordCtrl = meetingService->GetMeetingRecordingController(); + if (!recordCtrl) { + throw std::runtime_error("recording_controller_unavailable"); + } + + SDKError canRecord = recordCtrl->CanStartRawRecording(); + if (canRecord != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); + } + + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); + } + + audioHelper = GetAudioRawdataHelper(); + audioHelper->subscribe(&audioDelegate, true); + + createRenderer(&videoRenderer, &videoDelegate); + videoRenderer->setRawDataResolution(ZoomSDKResolution_720P); + videoRenderer->subscribe(activeUserId, RAW_DATA_TYPE_VIDEO); + } + + void stop(IMeetingService* meetingService) { + if (audioHelper) { + audioHelper->unSubscribe(); + } + if (videoRenderer) { + videoRenderer->unSubscribe(); + } + + auto* recordCtrl = meetingService->GetMeetingRecordingController(); + if (recordCtrl) { + recordCtrl->StopRawRecording(); + } + } + +private: + IZoomSDKAudioRawDataHelper* audioHelper = nullptr; + IZoomSDKRenderer* videoRenderer = nullptr; + MyAudioDelegate audioDelegate; + MyVideoDelegate videoDelegate; + uint32_t activeUserId = 0; +}; +``` + +Persisting a recording is your job after raw data arrives. Typical outputs are: + +- PCM audio -> WAV/FLAC encoder or streaming transcription pipeline +- YUV420 video -> FFmpeg transcode to MP4 or frame-by-frame CV pipeline +- Mixed bot pipeline -> raw capture first, then post-process after leave + +## Step 4: Subscribe to Raw Audio/Video + +```cpp +void subscribeToRawMedia() { + // Subscribe to raw audio + IZoomSDKAudioRawDataDelegate* audioDelegate = new MyAudioDelegate(); + SDKError audioErr = meetingService->GetMeetingAudioController() + ->GetMeetingAudioHelper() + ->subscribe(audioDelegate, true); // true = mixed audio + + if (audioErr != SDKERR_SUCCESS) { + cerr << "[AUDIO] Subscribe failed: " << audioErr << endl; + } else { + cout << "[AUDIO] Subscribed to mixed audio" << endl; + } + + // Subscribe to raw video + IZoomSDKVideoRawDataDelegate* videoDelegate = new MyVideoDelegate(); + SDKError videoErr = meetingService->GetMeetingVideoController() + ->GetMeetingVideoHelper() + ->subscribe(videoDelegate); + + if (videoErr != SDKERR_SUCCESS) { + cerr << "[VIDEO] Subscribe failed: " << videoErr << endl; + } else { + cout << "[VIDEO] Subscribed to video streams" << endl; + } +} +``` + +### Cloud Recording Alternative + +If the requirement is **Zoom-managed cloud recording** instead of raw media capture, use Meeting SDK only for the joining bot and use API/webhook skills for the recording workflow: + +```bash +curl -X POST "https://api.zoom.us/v2/users/{userId}/meetings" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Bot Recorded Meeting", + "type": 2, + "start_time": "2026-03-06T18:00:00Z", + "settings": { + "auto_recording": "cloud" + } + }' +``` + +Then subscribe to `recording.completed` and download assets through the recordings APIs: + +- `zoom-webhooks` -> receive `recording.completed` +- `zoom-rest-api` -> `GET /meetings/{meetingId}/recordings` or `GET /users/{userId}/recordings` + +Use this path when the desired output is Zoom-hosted MP4/M4A/transcript files rather than bot-owned raw PCM/YUV. + +## Step 5: Mid-Meeting Reconnection + +### Connection Monitoring + +```cpp +class MeetingBot { +private: + BotConfig config; + int reconnectionAttempt = 0; + +public: + void onMeetingStatusChanged(MeetingStatus status, int iResult) { + switch (status) { + case MEETING_STATUS_RECONNECTING: + cout << "[BOT] Connection lost, SDK is reconnecting..." << endl; + break; + + case MEETING_STATUS_FAILED: + case MEETING_STATUS_DISCONNECTING: + handleDisconnection(iResult); + break; + + case MEETING_STATUS_INMEETING: + // Reconnection successful + if (reconnectionAttempt > 0) { + cout << "[BOT] Reconnected successfully!" << endl; + reconnectionAttempt = 0; // Reset counter + } + break; + } + } + +private: + void handleDisconnection(int errorCode) { + reconnectionAttempt++; + + cout << "[RECONNECT] Disconnected (error: " << errorCode + << "), attempt " << reconnectionAttempt << "/" + << config.reconnect_max_attempts << endl; + + if (reconnectionAttempt >= config.reconnect_max_attempts) { + cerr << "[RECONNECT] Giving up after " + << reconnectionAttempt << " attempts" << endl; + cleanup(); + notifyFailure("Max reconnection attempts exceeded"); + return; + } + + // Exponential backoff: 2s, 4s, 8s... + int delay_ms = config.reconnect_base_delay_ms + * (1 << (reconnectionAttempt - 1)); + + cout << "[RECONNECT] Retrying in " << (delay_ms / 1000) << "s..." << endl; + + // Schedule reconnection + std::thread([this, delay_ms]() { + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + attemptRejoin(); + }).detach(); + } + + void attemptRejoin() { + // Re-use same meeting number, password, OBF token + // If OBF token expired, fetch new one from REST API first + + if (isOBFTokenExpired()) { + cout << "[RECONNECT] OBF token expired, fetching new one..." << endl; + // TODO: Call REST API to get fresh OBF token + } + + // Call join again + bool success = joinMeetingWithRetry( + cachedMeetingNumber, + cachedPassword, + cachedOBFToken, + cachedBotName + ); + + if (!success) { + cerr << "[RECONNECT] Rejoin failed" << endl; + cleanup(); + notifyFailure("Reconnection failed"); + } + } +}; +``` + +### Customizing Reconnection Behavior + +```cpp +// Example: Linear backoff instead of exponential +int delay_ms = config.reconnect_base_delay_ms * reconnectionAttempt; + +// Example: Capped exponential backoff (max 30s) +int delay_ms = std::min( + config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1)), + 30000 // Cap at 30s +); + +// Example: Jittered backoff (avoid thundering herd) +int base_delay = config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1)); +int jitter = rand() % 1000; // Random 0-1000ms +int delay_ms = base_delay + jitter; +``` + +## Recording Fallback: Local vs Cloud + +If raw recording fails, fall back to local or cloud recording: + +```cpp +void startRecordingWithFallback() { + IMeetingRecordingController* ctrl = + meetingService->GetMeetingRecordingController(); + + // Try raw recording first + if (ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + SDKError err = ctrl->StartRawRecording(); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using raw recording" << endl; + return; + } + } + + // Fallback: Local recording + if (ctrl->CanStartRecording(true) == SDKERR_SUCCESS) { + SDKError err = ctrl->StartRecording( + chrono::system_clock::now(), + "/tmp/bot_recording.mp4" + ); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using local recording" << endl; + return; + } + } + + // Fallback: Cloud recording + if (ctrl->CanStartCloudRecording() == SDKERR_SUCCESS) { + SDKError err = ctrl->StartCloudRecording(); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using cloud recording" << endl; + return; + } + } + + cerr << "[RECORDING] All recording methods failed" << endl; +} +``` + +## Complete Resilient Bot Example + +```cpp +int main() { + // 1. Load configuration + BotConfig config = loadConfig(); + + // 2. Initialize Meeting SDK + InitParam initParam; + initParam.strWebDomain = "https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + cerr << "InitSDK failed: " << err << endl; + return 1; + } + + // 3. Authenticate SDK with JWT + AuthContext authCtx; + authCtx.jwt_token = generateJWT(SDK_KEY, SDK_SECRET); + + IAuthService* authService = CreateAuthService(); + authService->SDKAuth(authCtx); + + // Wait for auth callback... + + // 4. Fetch meeting info + OBF token from REST API + MeetingInfo meetingInfo = fetchMeetingInfoFromAPI(MEETING_ID); + string obfToken = fetchOBFTokenFromAPI(USER_ID); + + if (meetingInfo.empty() || obfToken.empty()) { + cerr << "ABORT: Failed to get meeting info or OBF token" << endl; + return 1; + } + + // 5. Join meeting with retry + MeetingBot bot(config); + bool joined = bot.joinMeetingWithRetry( + meetingInfo.number, + meetingInfo.password, + obfToken, + "Transcription Bot" + ); + + if (!joined) { + cerr << "ABORT: Failed to join meeting" << endl; + return 1; + } + + // 6. SDK callbacks handle: StartRawRecording, subscribe, reconnection + + // 7. Keep running until meeting ends + bot.runEventLoop(); + + // 8. Cleanup + bot.cleanup(); + CleanUPSDK(); + + return 0; +} +``` + +## Comparison: Meeting SDK Bot vs RTMS Bot + +| Aspect | Meeting SDK Bot | RTMS Bot | +|--------|----------------|----------| +| **Visibility** | Visible participant | Invisible (read-only service) | +| **Authentication** | JWT + OBF token | REST API trigger + webhook | +| **Join Dependency** | Owner must be present | No dependency on participants | +| **Retry Logic** | Required (owner presence) | Not applicable (webhook-based) | +| **Media Access** | Raw audio/video/share via SDK | Audio/video/text/share/chat via WebSocket | +| **Recording Control** | Full (local, cloud, raw) | None (read-only) | +| **Interaction** | Can send chat, reactions | Cannot interact | +| **Resource Usage** | Higher (full SDK) | Lower (WebSocket only) | +| **Use Case** | Interactive bots, recording, moderation | Passive transcription, analytics | + +**Choose Meeting SDK Bot when:** +- You need to interact with the meeting (chat, reactions) +- You need local recording control +- You want to be visible in participant list +- You're processing your own meetings + +**Choose RTMS Bot when:** +- You only need to observe/transcribe +- You want minimal resource usage +- You prefer invisible operation +- You're processing external meetings + +## Troubleshooting + +### Bot Never Joins (OBF Owner Not Present) + +**Symptom:** All join attempts fail with "owner not in meeting" + +**Solution:** +1. Verify owner (OBF token holder) has joined the meeting +2. Increase `join_retry_attempts` or `join_retry_interval_ms` +3. Check meeting start time - don't attempt join before meeting starts +4. Consider webhook: Listen for `meeting.participant_joined` event for owner + +### Raw Recording Permission Denied + +**Symptom:** `CanStartRawRecording()` returns error + +**Solution:** +1. Admin → Account Settings → Meeting → In Meeting (Advanced) +2. Enable "Allow access to raw data (audio, video, sharing) for Meeting SDK" +3. Requires "Raw Data" entitlement from Zoom support + +### Frequent Disconnections During Meeting + +**Symptom:** Bot reconnects multiple times, then gives up + +**Solution:** +1. Increase `reconnect_max_attempts` (e.g., 5 instead of 3) +2. Increase `reconnect_base_delay_ms` if network is slow +3. Check server network stability +4. Monitor CPU/memory usage (insufficient resources can cause disconnects) + +### OBF Token Expires During Meeting + +**Symptom:** Reconnection fails with "invalid token" + +**Solution:** +1. Fetch fresh OBF token in `attemptRejoin()` before rejoining +2. Monitor token expiration (`expire_in` from API response) +3. Request longer-lived tokens (max TTL: 7200s = 2 hours) + +## Resources + +- **Meeting SDK Linux**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Raw Data Guide**: https://developers.zoom.us/docs/meeting-sdk/linux/add-features/raw-data/ +- **OBF FAQ**: https://developers.zoom.us/docs/meeting-sdk/obf-faq/ +- **REST API - Get Meeting**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meeting +- **REST API - Get OBF Token**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userToken +- **RTMS Bot Alternative**: [../../rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/linux/references/linux-reference.md b/partner-built/zoom-plugin/skills/meeting-sdk/linux/references/linux-reference.md new file mode 100644 index 00000000..3eef9000 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/linux/references/linux-reference.md @@ -0,0 +1,712 @@ +# Meeting SDK - Linux Reference + +Complete reference for Zoom Meeting SDK on Linux including dependencies, Docker setup, and troubleshooting. + +## System Requirements + +- **OS**: Ubuntu 22+, CentOS 8/9, Oracle Linux 8 +- **Architecture**: x86_64 +- **Build Tools**: cmake 3.16+, gcc/g++ with C++11 support + +## Dependencies + +### Ubuntu 22/23 + +```bash +# Build essentials +apt-get update +apt-get install -y build-essential cmake + +# X11/XCB libraries (required by SDK) +apt-get install -y --no-install-recommends --no-install-suggests \ + libx11-xcb1 \ + libxcb-xfixes0 \ + libxcb-shape0 \ + libxcb-shm0 \ + libxcb-randr0 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-xtest0 + +# Optional but recommended +apt-get install -y --no-install-recommends --no-install-suggests \ + libdbus-1-3 \ + libglib2.0-0 \ + libgbm1 \ + libxfixes3 \ + libgl1 \ + libdrm2 \ + libgssapi-krb5-2 + +# For curl-based JWT fetching +apt-get install -y \ + libcurl4-openssl-dev \ + openssl \ + ca-certificates \ + pkg-config + +# Audio support (PulseAudio) +apt-get install -y \ + libasound2 \ + libasound2-plugins \ + alsa \ + alsa-utils \ + alsa-oss \ + pulseaudio \ + pulseaudio-utils + +# Optional: FFmpeg for video processing +apt-get install -y libavformat-dev libavfilter-dev libavdevice-dev ffmpeg + +# If SDL2 errors occur +apt-get install -y libegl-mesa0 libsdl2-dev g++-multilib +``` + +### CentOS 8/9 + +```bash +# Build essentials +sudo yum install cmake gcc gcc-c++ + +# Enable required repos (CentOS 9) +sudo dnf config-manager --set-enabled crb +sudo dnf install epel-release epel-next-release + +# XCB libraries +sudo yum install \ + libxcb-devel \ + xcb-util-devel \ + xcb-util-image \ + xcb-util-keysyms + +# OpenGL/Mesa (for runtime) +sudo yum install \ + mesa-libGL \ + mesa-libGL-devel \ + mesa-dri-drivers + +# Curl support +sudo yum install -y openssl-devel libcurl-devel + +# PulseAudio +sudo yum install -y pulseaudio pulseaudio-utils + +# Optional: FFmpeg +sudo yum install -y libavformat-dev libavfilter-dev libavdevice-dev ffmpeg + +# If SDL2 errors occur +sudo yum install -y SDL2-devel +``` + +## CMakeLists.txt Template + +```cmake +cmake_minimum_required(VERSION 3.16) + +project(meetingSDKDemo CXX) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g -O0") +add_definitions(-std=c++11) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) + +find_package(PkgConfig REQUIRED) +find_package(ZLIB REQUIRED) + +# GLib (required for main loop) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(GIO REQUIRED gio-2.0) + +# Include directories +include_directories(${CMAKE_SOURCE_DIR}/include) +include_directories(${CMAKE_SOURCE_DIR}/include/h) +include_directories(${GLIB_INCLUDE_DIRS} ${GIO_INCLUDE_DIRS}) + +# Common GLib paths +include_directories(/usr/include/glib-2.0/) +include_directories(/usr/include/glib-2.0/glib) +include_directories(/usr/lib/x86_64-linux-gnu/glib-2.0/include/) +include_directories(/usr/lib64/glib-2.0/include/) + +# Link directories +link_directories(${GLIB_LIBRARY_DIRS} ${GIO_LIBRARY_DIRS}) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/qt_libs) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/qt_libs/Qt/lib) + +add_definitions(${GLIB_CFLAGS_OTHER} ${GIO_CFLAGS_OTHER}) + +# Source files +add_executable(meetingSDKDemo + ${CMAKE_SOURCE_DIR}/meeting_sdk_demo.cpp + ${CMAKE_SOURCE_DIR}/AuthServiceEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingServiceEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingReminderEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingParticipantsCtrlEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingRecordingCtrlEventListener.cpp + # Raw data handlers (if needed) + ${CMAKE_SOURCE_DIR}/ZoomSDKRenderer.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKAudioRawData.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKVideoSource.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKVirtualAudioMicEvent.cpp +) + +# Link libraries +target_link_libraries(meetingSDKDemo ${GLIB_LIBRARIES} ${GIO_LIBRARIES}) +target_link_libraries(meetingSDKDemo gcc_s gcc) +target_link_libraries(meetingSDKDemo meetingsdk) +target_link_libraries(meetingSDKDemo glib-2.0) +target_link_libraries(meetingSDKDemo curl) +target_link_libraries(meetingSDKDemo pthread) + +# Create symlink for SDK +execute_process(COMMAND ln -sf libmeetingsdk.so libmeetingsdk.so.1 + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk +) + +# Copy config to bin +configure_file(${CMAKE_SOURCE_DIR}/config.txt ${CMAKE_SOURCE_DIR}/bin/config.txt COPYONLY) + +# Copy SDK files to bin +file(COPY ${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/ DESTINATION ${CMAKE_SOURCE_DIR}/bin) +``` + +## Configuration File + +### config.txt Format + +``` +meeting_number: "1234567890" +token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +meeting_password: "abc123" +recording_token: "" +onBehalfOf_Token: "" +GetVideoRawData: "true" +GetAudioRawData: "true" +SendVideoRawData: "false" +SendAudioRawData: "false" +``` + +### config.json Format (Alternative) + +```json +{ + "meeting_number": "1234567890", + "token": "YOUR_JWT_TOKEN", + "meeting_password": "abc123", + "recording_token": "", + "remote_url": "https://your-auth-endpoint.com", + "useJWTTokenFromWebService": "false", + "useRecordingTokenFromWebService": "false" +} +``` + +## Docker Setup + +### Dockerfile (Ubuntu) + +```dockerfile +FROM ubuntu:22.04 + +# Prevent interactive prompts +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libdbus-1-3 libglib2.0-0 libgbm1 libxfixes3 libgl1 libdrm2 \ + libcurl4-openssl-dev openssl ca-certificates pkg-config \ + pulseaudio pulseaudio-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy project files +COPY . . + +# Build +RUN cd demo && cmake -B build && cd build && make + +# Setup audio config +RUN mkdir -p ~/.config && \ + echo "[General]" > ~/.config/zoomus.conf && \ + echo "system.audio.type=default" >> ~/.config/zoomus.conf + +CMD ["./demo/bin/meetingSDKDemo"] +``` + +### PulseAudio Setup (Headless) + +Create `setup-pulseaudio.sh`: + +```bash +#!/bin/bash + +# Start PulseAudio daemon +pulseaudio --start --exit-idle-time=-1 + +# Create virtual speaker +pactl load-module module-null-sink sink_name=virtual_speaker sink_properties=device.description="Virtual_Speaker" + +# Create virtual microphone +pactl load-module module-null-sink sink_name=virtual_mic sink_properties=device.description="Virtual_Microphone" + +# Create zoomus.conf +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << EOF +[General] +system.audio.type=default +EOF + +echo "PulseAudio configured for headless operation" +``` + +### Docker Compose + +```yaml +version: '3.8' +services: + meeting-bot: + build: . + environment: + - PULSE_SERVER=unix:/run/user/1000/pulse/native + volumes: + - ./config.txt:/app/demo/bin/config.txt + - /run/user/1000/pulse:/run/user/1000/pulse + network_mode: host +``` + +## Event Listeners + +### AuthServiceEventListener + +```cpp +// AuthServiceEventListener.h +#include "auth_service_interface.h" + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS && onAuthComplete) { + onAuthComplete(); + } + } + + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override {} + void onLogout() override {} + void onZoomIdentityExpired() override {} + void onZoomAuthIdentityExpired() override {} + +private: + void (*onAuthComplete)(); +}; +``` + +### MeetingServiceEventListener + +```cpp +// MeetingServiceEventListener.h +#include "meeting_service_interface.h" + +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() + ) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_CONNECTING) { + if (onMeetingJoined) onMeetingJoined(); + } + else if (status == MEETING_STATUS_INMEETING) { + if (onInMeetingCallback) onInMeetingCallback(); + } + else if (status == MEETING_STATUS_ENDED) { + if (onMeetingEnded) onMeetingEnded(); + } + } + + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool isActive) override {} + +private: + void (*onMeetingJoined)(); + void (*onMeetingEnded)(); + void (*onInMeetingCallback)(); +}; +``` + +### MeetingParticipantsCtrlEventListener + +```cpp +// MeetingParticipantsCtrlEventListener.h +#include "meeting_service_components/meeting_participants_ctrl_interface.h" + +class MeetingParticipantsCtrlEventListener : public IMeetingParticipantsCtrlEvent { +public: + MeetingParticipantsCtrlEventListener( + void (*onHost)(), + void (*onCoHost)() + ) : onIsHost(onHost), onIsCoHost(onCoHost) {} + + void onUserJoin(IList* lstUserID, const zchar_t* strUserList) override {} + void onUserLeft(IList* lstUserID, const zchar_t* strUserList) override {} + void onHostChangeNotification(unsigned int userId) override { + if (onIsHost) onIsHost(); + } + void onCoHostChangeNotification(unsigned int userId, bool isCoHost) override { + if (isCoHost && onIsCoHost) onIsCoHost(); + } + void onLowOrRaiseHandStatusChanged(bool bLow, unsigned int userid) override {} + +private: + void (*onIsHost)(); + void (*onIsCoHost)(); +}; +``` + +## Raw Data Requirements + +### Recording Permission + +Raw data access requires one of: +1. **Host** status +2. **Co-host** status +3. **Recording permission** from host +4. **Recording token** (app_privilege_token) + +```cpp +// Check and start raw recording +void CheckAndStartRawRecording() { + IMeetingRecordingController* recordCtrl = + m_pMeetingService->GetMeetingRecordingController(); + + SDKError canStart = recordCtrl->CanStartRawRecording(); + + if (canStart == SDKERR_SUCCESS) { + recordCtrl->StartRawRecording(); + // Now subscribe to raw data + } else { + std::cout << "Need host/cohost/recording permission" << std::endl; + } +} +``` + +### Video Resolutions + +```cpp +videoHelper->setRawDataResolution(ZoomSDKResolution_90P); +videoHelper->setRawDataResolution(ZoomSDKResolution_180P); +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); +``` + +### Audio Format + +- **Format**: PCM (raw) +- **Sample Rate**: 32000 Hz +- **Channels**: Mono or Stereo +- **Bit Depth**: 16-bit + +## Compilation Tips + +> **Note**: Specific methods/types may vary by SDK version. Always check SDK headers. + +### Event Listener Interfaces +SDK event listener interfaces have many pure virtual methods. **Implement ALL of them** (even with empty bodies) or you'll get "invalid new-expression of abstract class type" errors. Check the SDK header for the complete interface definition. + +### Include Order Issues +Some SDK headers don't include their dependencies. If you get undefined type errors: +- Add standard includes (``, ``) before SDK headers +- Include related SDK headers in dependency order (e.g., `meeting_audio_interface.h` before `meeting_participants_ctrl_interface.h`) + +### Method Signatures +Reference samples may have outdated method names. The SDK header files are the authoritative source - always verify method signatures there. + +## Troubleshooting + +### Segmentation Fault on AuthSDK + +**Cause**: OpenSSL version incompatibility (needs 1.1.x) + +**Fix** (Ubuntu): +```bash +cd /tmp +wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.20_amd64.deb +sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.20_amd64.deb +``` + +### libGL Error (Mesa) + +``` +libGL error: MESA-LOADER: failed to open swrast +``` + +**Fix**: +```bash +# Ubuntu +apt-get install mesa-libGL mesa-dri-drivers + +# CentOS +yum install mesa-libGL mesa-libGL-devel mesa-dri-drivers +``` + +### XCB Libraries Missing + +``` +libxcb-image.so.0 not found +libxcb-keysyms.so.1 not found +``` + +**Fix**: +```bash +# Ubuntu +apt-get install libxcb-image0 libxcb-keysyms1 + +# CentOS +yum install xcb-util-image xcb-util-keysyms +``` + +### No Audio in Docker + +**Fix**: Create zoomus.conf before running: +```bash +mkdir -p ~/.config +echo -e "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf +``` + +### Cannot Start Raw Recording + +**Cause**: No recording permission + +**Fix**: Either: +1. Wait for host to grant recording permission +2. Use `recording_token` in config (get from [Recording Token API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken)) +3. Join as host using `onBehalfOf_Token` + +## Cleanup + +```cpp +void CleanSDK() { + if (videoHelper) videoHelper->unSubscribe(); + if (audioHelper) audioHelper->unSubscribe(); + + if (m_pAuthService) { + DestroyAuthService(m_pAuthService); + m_pAuthService = NULL; + } + if (m_pSettingService) { + DestroySettingService(m_pSettingService); + m_pSettingService = NULL; + } + if (m_pMeetingService) { + DestroyMeetingService(m_pMeetingService); + m_pMeetingService = NULL; + } + + CleanUPSDK(); +} +``` + +## API Reference + +### InitParam Structure + +```cpp +struct tagInitParam { + const zchar_t* strWebDomain; // "https://zoom.us" + const zchar_t* strBrandingName; // Custom branding name + const zchar_t* strSupportUrl; // Support URL + SDK_LANGUAGE_ID emLanguageID; // LANGUAGE_English, etc. + bool enableGenerateDump; // Enable crash dump + bool enableLogByDefault; // Enable logging + unsigned int uiLogFileSize; // Log file size (MB, default: 5) + RawDataOptions rawdataOpts; // Raw data options + ConfigurableOptions obConfigOpts; // Config options + int wrapperType; // SDK wrapper type +}; +``` + +### IAuthService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IAuthServiceEvent*)` | Set auth callback | `SDKError` | +| `SDKAuth(AuthContext&)` | Authenticate with JWT | `SDKError` | +| `GetAuthResult()` | Get auth status | `AuthResult` | +| `GetSDKIdentity()` | Get SDK identity | `const zchar_t*` | +| `LogOut()` | Logout | `SDKError` | +| `GetAccountInfo()` | Get account info | `IAccountInfo*` | +| `GetLoginStatus()` | Get login status | `LOGINSTATUS` | + +### IMeetingService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IMeetingServiceEvent*)` | Set meeting callback | `SDKError` | +| `Join(JoinParam&)` | Join meeting | `SDKError` | +| `Start(StartParam&)` | Start meeting | `SDKError` | +| `Leave(LeaveMeetingCmd)` | Leave meeting | `SDKError` | +| `GetMeetingStatus()` | Get status | `MeetingStatus` | +| `GetMeetingInfo()` | Get meeting info | `IMeetingInfo*` | +| `GetMeetingVideoController()` | Video control | `IMeetingVideoController*` | +| `GetMeetingAudioController()` | Audio control | `IMeetingAudioController*` | +| `GetMeetingRecordingController()` | Recording control | `IMeetingRecordingController*` | +| `GetMeetingParticipantsController()` | Participants | `IMeetingParticipantsController*` | +| `GetMeetingChatController()` | Chat control | `IMeetingChatController*` | + +### JoinParam4WithoutLogin Structure + +```cpp +struct JoinParam4WithoutLogin { + UINT64 meetingNumber; // Meeting number + const zchar_t* userName; // Display name + const zchar_t* psw; // Meeting password + const zchar_t* vanityID; // Personal link name + const zchar_t* customer_key; // Customer key + const zchar_t* webinarToken; // Webinar token + const zchar_t* userZAK; // Zoom Access Key + const zchar_t* app_privilege_token; // App privilege token (for raw data) + const zchar_t* onBehalfToken; // On behalf token + bool isVideoOff; // Start with video off + bool isAudioOff; // Start with audio off +}; +``` + +### IZoomSDKRenderer Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `setRawDataResolution(ZoomSDKResolution)` | Set resolution | `SDKError` | +| `subscribe(uint32_t userId, ZoomSDKRawDataType)` | Subscribe to video | `SDKError` | +| `unSubscribe()` | Unsubscribe | `SDKError` | +| `getResolution()` | Get resolution | `ZoomSDKResolution` | +| `getSubscribeId()` | Get user ID | `uint32_t` | + +**ZoomSDKResolution values:** `ZoomSDKResolution_90P`, `_180P`, `_360P`, `_720P`, `_1080P` + +### YUVRawDataI420 Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetStreamWidth()` | Video width | `uint32_t` | +| `GetStreamHeight()` | Video height | `uint32_t` | +| `GetYBuffer()` | Y plane buffer | `char*` | +| `GetUBuffer()` | U plane buffer | `char*` | +| `GetVBuffer()` | V plane buffer | `char*` | +| `GetBufferLen()` | Total buffer length | `uint32_t` | +| `GetRotation()` | Rotation angle | `int` | +| `GetAlphaBuffer()` | Alpha channel | `char*` | + +**Video Format:** +- YUV420 (I420) contiguous planar format (no strides) +- Y plane: `width * height` bytes +- U plane: `(width/2) * (height/2)` bytes +- V plane: `(width/2) * (height/2)` bytes +- Total size: `width * height * 1.5` bytes + +### AudioRawData Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetBuffer()` | Audio buffer | `char*` | +| `GetBufferLen()` | Buffer length (bytes) | `uint32_t` | +| `GetSampleRate()` | Sample rate (Hz) | `uint32_t` | +| `GetChannelNum()` | Number of channels | `uint32_t` | + +**Audio Format:** +- PCM (uncompressed), 16-bit, little-endian +- Sample rate: 32000 Hz (typical) +- Channels: 1 (mono) or 2 (stereo) + +### Playing Raw Files with FFmpeg + +Raw files have no headers - specify format explicitly: + +```bash +# Play YUV video (adjust dimensions to match your output) +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv + +# Convert YUV to MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 + +# Play PCM audio +ffplay -f s16le -ar 32000 -ac 1 audio.pcm + +# Convert PCM to WAV +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav + +# Combine video + audio into MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +### Error Codes (SDKError) + +| Code | Description | +|------|-------------| +| `SDKERR_SUCCESS` | Success | +| `SDKERR_INVALID_PARAMETER` | Invalid parameter | +| `SDKERR_UNINITIALIZE` | SDK not initialized | +| `SDKERR_UNAUTHENTICATION` | Not authenticated | +| `SDKERR_NO_PERMISSION` | No permission | +| `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device | +| `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device | +| `SDKERR_INTERNAL_ERROR` | Internal error | +| `SDKERR_SERVICE_FAILED` | Service failed | +| `SDKERR_MEMORY_FAILED` | Memory allocation failed | +| `SDKERR_TOO_FREQUENT_CALL` | API called too frequently | + +### Authentication Results (AuthResult) + +| Result | Description | +|--------|-------------| +| `AUTHRET_SUCCESS` | Authentication successful | +| `AUTHRET_KEYORSECRETEMPTY` | Key or secret empty | +| `AUTHRET_JWTTOKENWRONG` | JWT token invalid | +| `AUTHRET_OVERTIME` | Operation timed out | + +### Meeting Status (MeetingStatus) + +| Status | Description | +|--------|-------------| +| `MEETING_STATUS_IDLE` | No meeting | +| `MEETING_STATUS_CONNECTING` | Connecting | +| `MEETING_STATUS_INMEETING` | In meeting | +| `MEETING_STATUS_RECONNECTING` | Reconnecting | +| `MEETING_STATUS_FAILED` | Failed | +| `MEETING_STATUS_ENDED` | Meeting ended | +| `MEETING_STATUS_WAITINGFORHOST` | Waiting for host | + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Options: +- **App Privilege Token (OBF)** - Recommended for bots +- **ZAK Token** - Zoom Access Key +- **On Behalf Token** - For specific use cases + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/index.html +- **Headless sample**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Raw recording sample**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- **Auth endpoint**: https://github.com/zoom/meetingsdk-auth-endpoint-sample diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/RUNBOOK.md new file mode 100644 index 00000000..667b93be --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK macOS 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for macOS (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/macos/ +- https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/macos/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/macos/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/SKILL.md new file mode 100644 index 00000000..c170bf34 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/SKILL.md @@ -0,0 +1,41 @@ +--- +name: zoom-meeting-sdk-macos +description: | + Zoom Meeting SDK for macOS native apps. Use when embedding Zoom meetings in macOS with + default/custom UI, PKCE + SDK auth, host start/join flows, and desktop meeting feature controllers. +user-invocable: false +triggers: + - "meeting sdk macos" + - "zoom macos sdk" + - "mac sdk meeting" + - "mac custom ui" + - "mac default ui" + - "join meeting mac" + - "start meeting mac" +--- + +# Zoom Meeting SDK (macOS) + +Use this skill when building macOS apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [macos.md](macos.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/macos-reference-map.md](references/macos-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/macos/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/architecture.md new file mode 100644 index 00000000..7058d3a9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/architecture.md @@ -0,0 +1,23 @@ +# macOS Architecture + +## Layer Model + +- App shell (AppKit/Swift UI integration layer). +- Meeting coordinator (join/start state machine). +- SDK service/controller layer (ZoomSDK class + service delegates). +- Backend signing/token service. + +## Reference Flow + +```text +macOS App -> Meeting Coordinator -> Backend Signature Service -> Meeting SDK + ^ | | | + | v v v +User actions State + retry policy Role/token policy Service delegates +``` + +## Why this split + +- Isolates security logic from desktop client. +- Makes controller/delegate ordering explicit. +- Reduces upgrade regression blast radius. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..23b34298 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# macOS Lifecycle Workflow + +## Core Sequence + +1. App startup and SDK initialization. +2. SDK auth callback success. +3. Join/start flow selection. +4. Meeting controller registration and in-meeting feature activation. +5. Leave/end handling. +6. SDK cleanup and process teardown. + +## Failure Domains + +- Auth/signature mismatch. +- Join/start parameter mismatch. +- Delegate/controller ordering issues in custom UI mode. +- Feature-level permission or role mismatch (recording, breakout, webinar). diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/examples/join-start-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/examples/join-start-pattern.md new file mode 100644 index 00000000..38104502 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# macOS Join/Start Pattern + +## Join (attendee) + +1. Get short-lived signature from backend. +2. Initialize/auth SDK and verify callback result. +3. Join with meeting number + passcode. +4. Register required meeting delegates before user interaction. + +## Start (host) + +1. Backend provides host `ZAK` + role-aware signature. +2. Start flow executes with host token. +3. Host-only controls enabled after privilege verification. + +## Guardrails + +- Keep SDK secret off client. +- Validate delegate callbacks under both default and custom UI. +- Explicitly handle leave/end transitions for cleanup. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/macos.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/macos.md new file mode 100644 index 00000000..dbe9fae9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/macos.md @@ -0,0 +1,17 @@ +# Meeting SDK macOS Guide + +## Scope + +macOS Meeting SDK integration for default/custom UI, auth, join/start, and in-meeting feature controllers. + +## Validation Snapshot + +- Docs coverage includes: get-started, integrate, start/join/auth paths, default UI, custom UI, service quality, and error-code pages. +- API reference snapshot includes class/protocol maps, globals/functions pages, and file-level references. +- Local package checked: `zoom-sdk-macos-6.7.6.75900` with `ZoomSDKSample` and native macOS app sources. + +## Practical Guidance + +1. Get stable default UI flow first. +2. Add custom UI and advanced controls incrementally. +3. Validate immersive/share/annotation feature paths after each SDK upgrade. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/environment-variables.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/environment-variables.md new file mode 100644 index 00000000..afd063fb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/environment-variables.md @@ -0,0 +1,15 @@ +# macOS Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep signing on backend. +- For desktop distribution, keep runtime token handling outside checked-in configs. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/macos-reference-map.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/macos-reference-map.md new file mode 100644 index 00000000..eee0c2fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/macos-reference-map.md @@ -0,0 +1,33 @@ +# macOS Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/macos/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `45` +- API reference pages captured: `528` + +## Key API Entry Pages + +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions*` +- `globals*` +- `pages.md` + +## Notable API Surface Areas + +- ZoomSDK service/controller interfaces +- Meeting/audio/video/share/webinar/breakout modules +- AI companion, smart summary, and avatar related interfaces +- Raw data helper/delegate interfaces + +## Drift Signals to Watch + +- Frequent growth in `globals*` and controller interfaces. +- Added AI Companion and smart-summary-related surfaces across recent versions. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/versioning-and-compatibility.md new file mode 100644 index 00000000..9a7b7c5b --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# macOS Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.6.75900` +- Docs baseline: current macOS Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK package and re-test controller/delegate contracts on upgrade. +- Verify custom UI features (annotation/share/immersive) first during upgrade testing. +- Maintain a release checklist for host-only and webinar-specific flows. + +## Contradiction/Drift Notes + +- Docs contain both top-level and nested advanced-feature paths; treat them as parallel documentation organization, not separate APIs. +- Changelog in package is external-link based; maintain local upgrade notes for exact behavior changes. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..b31d07f7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# macOS High-Level Scenarios + +## Scenario 1: Enterprise desktop collaboration app + +- Default UI embed for fast rollout. +- Adds policy-based host controls. +- Logs meeting lifecycle and quality stats for support analytics. + +## Scenario 2: Media-rich custom meeting app + +- Uses custom UI for branded layout and controlled tool access. +- Integrates share/annotation/immersive features. +- Keeps default UI fallback for release safety. + +## Scenario 3: Training operations console + +- Hosts meetings with role-aware controls. +- Uses breakout and participant controllers. +- Adds upgrade compatibility checks before broad deployment. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/macos/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/macos/troubleshooting/common-issues.md new file mode 100644 index 00000000..41bbdb9a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/macos/troubleshooting/common-issues.md @@ -0,0 +1,22 @@ +# macOS Common Issues + +## 1. Join/start flow errors + +- Validate signature freshness and role. +- Confirm meeting identifier and passcode mapping. +- Validate host token (`ZAK`) for start path. + +## 2. Delegate callback gaps + +- Ensure delegate/controller registration happens before feature usage. +- Keep coordinator/service objects strongly referenced through session lifecycle. + +## 3. Custom UI regressions + +- Verify default UI still works to isolate custom layer issues. +- Re-check rendering and feature-controller dependencies after SDK upgrades. + +## 4. Version drift + +- Re-run feature-level tests on breakout, share, annotation, and AI companion modules. +- Compare API reference map for renamed or newly required interfaces. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/RUNBOOK.md new file mode 100644 index 00000000..a991ccd6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK React Native 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for React Native (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/react-native/ +- https://marketplacefront.zoom.us/sdk/meeting/reactnative/modules.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/react-native/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/reactnative/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/SKILL.md new file mode 100644 index 00000000..363977e4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/SKILL.md @@ -0,0 +1,106 @@ +--- +name: zoom-meeting-sdk-react-native +description: Zoom Meeting SDK for React Native. Use when embedding Zoom meetings in React Native iOS/Android apps with @zoom/meetingsdk-react-native, JWT auth, join/start flows, platform setup, and native bridge troubleshooting. +user-invocable: false +triggers: + - react native meeting sdk + - zoom react native + - meetingsdk-react-native + - join meeting in react native + - start meeting with zak + - zoom mobile sdk wrapper +--- + +# Zoom Meeting SDK (React Native) + +Use this skill when building React Native apps that need embedded Zoom meeting join/start flows. + +## Quick Links + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> auth -> join/start -> in-meeting -> cleanup +2. **[Architecture](concepts/architecture.md)** - JS wrapper, native bridge, iOS/Android SDK layers +3. **[High-Level Scenarios](concepts/high-level-scenarios.md)** - practical product patterns +4. **[Setup Guide](examples/setup-guide.md)** - install package + platform requirements +5. **[Join Meeting Pattern](examples/join-meeting-pattern.md)** - JWT + meetingNumber + password +6. **[Start Meeting Pattern](examples/start-meeting-pattern.md)** - ZAK-based host start +7. **[SKILL.md](SKILL.md)** - full navigation + +## Core APIs (Wrapper) + +From `@zoom/meetingsdk-react-native` wrapper surface: + +- `initSDK(config)` +- `isInitialized()` +- `updateMeetingSetting(config)` +- `joinMeeting(config)` +- `startMeeting(config)` +- `cleanup()` + +See: **[Wrapper API](references/wrapper-api.md)** + +## Critical Notes + +- You still need native iOS/Android Meeting SDK dependencies configured. +- `joinMeeting` and `startMeeting` return numeric status/error codes from native layer. +- For host start flow, pass `zoomAccessToken` (ZAK). +- Keep JWT generation on backend, never embed SDK secret in app. +- Current docs note React Native support up to `0.75.4`; Expo is not supported. + +## Platform Guides + +- **[iOS Setup](references/ios-setup.md)** - Podfile, optional ReplayKit/app group fields +- **[Android Setup](references/android-setup.md)** - Gradle dependency + options mapping +- **[Native Bridge Notes](references/native-bridge-notes.md)** - behavior differences and gotchas + +## Troubleshooting + +- **[Common Issues](troubleshooting/common-issues.md)** +- **[Version Drift](troubleshooting/version-drift.md)** +- **[Deprecated/Contradictions](troubleshooting/deprecated-and-contradictions.md)** + +## Related Skills + +- **[zoom-meeting-sdk](../SKILL.md)** - parent Meeting SDK hub +- **[zoom-oauth](../../oauth/SKILL.md)** - auth flow and token management +- **[zoom-general](../../general/SKILL.md)** - cross-product architecture decisions + +## Documentation Index + +### Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [Architecture](concepts/architecture.md) +4. [Setup Guide](examples/setup-guide.md) + +### Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [Architecture](concepts/architecture.md) +- [Auth and Token Model](concepts/auth-and-token-model.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +### Examples + +- [Setup Guide](examples/setup-guide.md) +- [Join Meeting Pattern](examples/join-meeting-pattern.md) +- [Start Meeting Pattern](examples/start-meeting-pattern.md) +- [Provider Hook Pattern](examples/provider-hook-pattern.md) + +### References + +- [Wrapper API](references/wrapper-api.md) +- [Android Setup](references/android-setup.md) +- [iOS Setup](references/ios-setup.md) +- [Native Bridge Notes](references/native-bridge-notes.md) +- [Official Sources](references/official-sources.md) + +### Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/architecture.md new file mode 100644 index 00000000..86154deb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/architecture.md @@ -0,0 +1,16 @@ +# Architecture + +The React Native package is a wrapper around native Meeting SDKs. + +## Layers + +- JS API layer: `@zoom/meetingsdk-react-native` +- Context/hook layer: `ZoomSDKProvider`, `useZoom` +- Native module layer: `RNZoomSDK` (iOS Obj-C, Android Java) +- Zoom native SDK layer: MobileRTC (iOS) and ZoomSDK (Android) + +## Why this matters + +- Wrapper updates can change JS signatures while native SDK versions evolve independently. +- Some options are platform-specific (`logSize` Android, `bundleResPath` iOS). +- Numeric error codes from native must be interpreted by platform docs. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md new file mode 100644 index 00000000..94a1b0f3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md @@ -0,0 +1,17 @@ +# Auth and Token Model + +## Token types used by wrapper + +- `jwtToken` in `initSDK`: Meeting SDK JWT (for SDK authorization) +- `zoomAccessToken` in `startMeeting`: ZAK token for host start + +## Security model + +- Generate tokens server-side only. +- Never ship SDK secret in the app. +- Keep JWT short-lived and rotate aggressively. + +## Flow guidance + +- Participant join: `initSDK(jwtToken)` + `joinMeeting(meetingNumber, password)` +- Host start: `initSDK(jwtToken)` + `startMeeting(zoomAccessToken=ZAK, meetingNumber)` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md new file mode 100644 index 00000000..bd92bc3c --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md @@ -0,0 +1,25 @@ +# High-Level Scenarios + +## 1. Mobile attendee app (customer-facing) + +- User opens iOS/Android app and joins meetings from in-app schedule. +- Backend provides short-lived Meeting SDK JWT. +- App calls `joinMeeting` with meeting number and passcode when required. + +## 2. Mobile host operations app + +- Authenticated operator starts scheduled sessions from mobile. +- Backend retrieves host ZAK via Zoom APIs. +- App uses `startMeeting` with `zoomAccessToken`. + +## 3. Kiosk-style controlled join flow + +- App runs in constrained device mode. +- Meeting controls are reduced via meeting flags/settings. +- App enforces deterministic init -> join -> cleanup sequence on each session. + +## 4. Support/field workforce app + +- Agents join support calls and optionally use share/chat controls. +- Per-platform settings are tuned independently (Android/iOS parity is not guaranteed). +- Runtime fallback handling is added for unsupported feature flags. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..9b19462a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md @@ -0,0 +1,20 @@ +# Lifecycle Workflow + +Recommended runtime flow: + +1. App bootstraps and wraps tree with `ZoomSDKProvider`. +2. `initSDK` runs once with `jwtToken`, `domain`, logging options. +3. App checks `isInitialized()` before meeting actions. +4. User chooses: + - `joinMeeting` (participant) + - `startMeeting` (host, with ZAK) +5. Native Meeting SDK UI/session runs. +6. Call `cleanup()` on app shutdown/logout. + +```text +React UI -> ZoomSDKProvider -> JS Wrapper (ZoomSDK.ts) + -> Native Bridge (RNZoomSDK) + -> iOS MobileRTC / Android ZoomSDK +``` + +If initialization/auth fails, stop and rotate token before retrying. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md new file mode 100644 index 00000000..5a3e9e59 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md @@ -0,0 +1,19 @@ +# Join Meeting Pattern + +```tsx +import { useZoom } from '@zoom/meetingsdk-react-native'; + +const zoom = useZoom(); + +await zoom.joinMeeting({ + userName: 'participant-name', + meetingNumber: '123456789', + password: 'meeting-password', + userType: 1, +}); +``` + +Notes: + +- `meetingNumber` and `userName` are required by wrapper validation. +- `password` is optional in API shape, but may be mandatory by meeting settings. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md new file mode 100644 index 00000000..b5526d3c --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md @@ -0,0 +1,14 @@ +# Provider Hook Pattern + +Use wrapper context consistently. + +```tsx +import { ZoomSDKProvider, useZoom } from '@zoom/meetingsdk-react-native'; + +function MeetingActions() { + const zoom = useZoom(); + // zoom.joinMeeting / zoom.startMeeting / zoom.cleanup +} +``` + +Do not call wrapper methods before provider initialization is complete. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/setup-guide.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/setup-guide.md new file mode 100644 index 00000000..9193dfe2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/setup-guide.md @@ -0,0 +1,43 @@ +# Setup Guide + +## 1. Install package + +```bash +npm install @zoom/meetingsdk-react-native +``` + +## 2. Respect documented support boundaries + +- React Native support currently documented up to `0.75.4`. +- Expo is currently not supported. +- Android baseline from docs: `minSdkVersion = 26`, `targetSdkVersion = 35`. + +## 3. Align platform SDK versions + +- This wrapper does not bundle native iOS/Android Meeting SDK artifacts for all workflows. +- Keep wrapper and native Meeting SDK versions aligned. +- For older wrapper versions (pre-`6.4.5`), docs note manual native SDK placement may be required. + +## 4. Initialize provider + +```tsx +import { ZoomSDKProvider } from '@zoom/meetingsdk-react-native'; + +', + domain: 'zoom.us', + enableLog: true, + logSize: 5, + }} +> + + +``` + +## 5. Platform prerequisites + +- Android: include Zoom Meeting SDK dependency in gradle and required permissions. +- iOS: ensure Podfile and framework setup are aligned with package expectations. + +See [Android Setup](../references/android-setup.md) and [iOS Setup](../references/ios-setup.md). diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md new file mode 100644 index 00000000..fde3048d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md @@ -0,0 +1,18 @@ +# Start Meeting Pattern + +```tsx +import { useZoom } from '@zoom/meetingsdk-react-native'; + +const zoom = useZoom(); + +await zoom.startMeeting({ + userName: 'host-name', + meetingNumber: '123456789', + zoomAccessToken: '', +}); +``` + +Notes: + +- `zoomAccessToken` is required for host start in wrapper validation. +- Missing/expired ZAK returns native start failure code. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/android-setup.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/android-setup.md new file mode 100644 index 00000000..8988b6d7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/android-setup.md @@ -0,0 +1,15 @@ +# Android Setup Notes + +Representative dependency from SDK package example: + +```gradle +implementation('us.zoom.meetingsdk:zoomsdk:6.7.2') +``` + +Other observed setup details: + +- Java/Kotlin target 17 in sample. +- Wrapper maps many `JoinMeetingOptions` / `StartMeetingOptions` flags. +- `language` setting is consumed by native bridge during `updateMeetingSetting`. + +Always verify against your app's RN/Gradle/Kotlin compatibility matrix. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/ios-setup.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/ios-setup.md new file mode 100644 index 00000000..47165da4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/ios-setup.md @@ -0,0 +1,13 @@ +# iOS Setup Notes + +Observed in package sample project: + +- Podfile includes React Native integration and required permissions pods. +- Wrapper supports optional init fields: + - `bundleResPath` + - `appGroupId` + - `replaykitBundleIdentifier` + +These are relevant for custom resource path and screen-share style setups. + +Confirm iOS deployment target and Podfile settings against your RN version. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/native-bridge-notes.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/native-bridge-notes.md new file mode 100644 index 00000000..477499b0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/native-bridge-notes.md @@ -0,0 +1,16 @@ +# Native Bridge Notes + +## Android bridge + +- Uses `ZoomSDK.initialize(...)` with `wrapperType = 2`. +- `joinMeeting` and `startMeeting` resolve numeric result codes. +- Exposes lifecycle hooks but event emitter list is currently empty in bridge. + +## iOS bridge + +- Initializes `MobileRTC` + auth service (`sdkAuth` with JWT). +- `joinMeeting`/`startMeeting` call native meeting service methods and resolve/reject promises. + +## Practical implication + +The wrapper currently behaves as command-based API with limited cross-platform event exposure. Build app-level state handling around command results and native UI transitions. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/official-sources.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/official-sources.md new file mode 100644 index 00000000..2e47ab55 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/official-sources.md @@ -0,0 +1,14 @@ +# Official Sources + +Primary sources used for this skill: + +- Zoom docs (React Native Meeting SDK): https://developers.zoom.us/docs/meeting-sdk/react-native/ +- Zoom reference (React Native modules): https://marketplacefront.zoom.us/sdk/meeting/reactnative/modules.html +- Zoom Meeting SDK React Native package 6.7.2 (local archive analysis) +- Crawled docs snapshots under `skills/raw-docs/developers.zoom.us/docs/meeting-sdk/react-native/` +- Crawled reference snapshots under `skills/raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/reactnative/` + +Related: + +- Meeting SDK auth: https://developers.zoom.us/docs/meeting-sdk/auth/ +- Quickstart repo: https://github.com/zoom/MeetingSDK-ReactNative-Quickstart diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/wrapper-api.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/wrapper-api.md new file mode 100644 index 00000000..c87b98af --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/references/wrapper-api.md @@ -0,0 +1,30 @@ +# Wrapper API Reference (React Native package) + +## Init config + +- `jwtToken?: string` +- `domain?: string` +- `enableLog?: boolean` +- `logSize?: number` (Android) +- `bundleResPath?: string` (iOS) +- `appGroupId?: string` (iOS) +- `replaykitBundleIdentifier?: string` (iOS) + +## Methods + +- `initSDK(config): Promise` +- `isInitialized(): Promise` +- `joinMeeting(config): Promise` +- `startMeeting(config): Promise` +- `updateMeetingSetting(config): void` +- `cleanup(): void` + +## Join config highlights + +- Required: `userName`, `meetingNumber` +- Optional: `password`, `zoomAccessToken`, `vanityID`, `webinarToken`, `joinToken`, `appPrivilegeToken` + +## Start config highlights + +- Required: `userName`, `zoomAccessToken` +- Optional: `meetingNumber`, `vanityID`, `inviteContactId` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/common-issues.md new file mode 100644 index 00000000..2c4fdb9e --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/common-issues.md @@ -0,0 +1,24 @@ +# Common Issues + +## `joinMeeting` fails immediately + +- Validate meeting number format and password. +- Confirm SDK initialization succeeded first. +- Check JWT validity window. + +## `startMeeting` fails + +- Verify `zoomAccessToken` (ZAK) is present and unexpired. +- Ensure host account and meeting ownership match ZAK context. + +## Provider/hook misuse + +- Ensure components calling `useZoom()` are wrapped in `ZoomSDKProvider`. + +## iOS-specific init issues + +- Confirm optional fields (`bundleResPath`, `appGroupId`, `replaykitBundleIdentifier`) only when needed and correctly configured. + +## Android language crash risk + +- Avoid passing partial/invalid language values to `updateMeetingSetting`. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..8e90b493 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,27 @@ +# Deprecated and Contradictions Notes + +Observed from analyzed package/source artifacts: + +1. Reference URL naming inconsistency +- Package README references `react-native/annotated.html` for full API list, while current typed reference entrypoint is `modules.html`. +- Treat this as documentation routing inconsistency across versions. + +2. Meeting SDK vs Video SDK wording inconsistency in docs +- Crawled React Native docs include wording that the Meeting SDK wrapper is based on the native Video SDK version. +- Treat this as a likely docs wording issue; align implementation to Meeting SDK package/version compatibility docs. + +3. Example UX vs API shape mismatch +- Example UI prompts "Password Optional" but code blocks join if password is empty. +- Wrapper type allows optional `password`; runtime behavior depends on meeting config. + +4. Android bridge fragility +- `updateMeetingSetting` uses `config.getString("language")` without robust null checks before split. +- Passing missing/invalid language can crash or throw. + +5. Event model limitations +- Android bridge includes emitter plumbing but empty supported event set. +- Do not assume parity with native event coverage without custom extension. + +6. Version/toolchain caveat in demo docs +- Demo notes include version-conditional setup (for example pre-`6.4.5` artifact handling) and non-Expo limitation. +- Keep integration guides version-scoped to avoid false assumptions during upgrades. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/version-drift.md b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/version-drift.md new file mode 100644 index 00000000..1b637293 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/react-native/troubleshooting/version-drift.md @@ -0,0 +1,17 @@ +# Version Drift Guidance + +Because wrapper and native SDKs evolve: + +- Reconfirm option names on each wrapper upgrade. +- Treat returned numeric meeting codes as versioned behavior. +- Re-test both join and host-start flows after SDK bump. +- Validate platform-specific flags (Android/iOS) separately. +- Reconfirm React Native framework compatibility window and Expo support status. + +Upgrade checklist: + +1. Compare `src/native/ZoomSDK.ts` API types between versions. +2. Compare Android `RNZoomSDKModule.java` option mapping. +3. Compare iOS `RNZoomSDK.m` auth/join/start implementations. +4. Re-run smoke tests: init -> isInitialized -> join/start -> cleanup. +5. Re-validate Android/iOS setup requirements from docs (permissions, min/target SDK, pod/gradle notes). diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/ai-companion.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/ai-companion.md new file mode 100644 index 00000000..622d8e7a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/ai-companion.md @@ -0,0 +1,225 @@ +# AI Companion in Meeting SDK + +Control AI Companion features in embedded Zoom meetings. + +## Overview + +The Meeting SDK provides `InMeetingAICompanionController` to manage AI Companion features within meetings. This allows you to check status and available features. + +**Important**: The Meeting SDK is for human use cases only. It does NOT support bots or AI notetakers. For bot/automated AI processing, use **rtms**. + +## Availability + +| Platform | Controller Available | +|----------|---------------------| +| Web | Limited (settings-based) | +| Android | ✅ `InMeetingAICompanionController` | +| iOS | ✅ `MobileRTCAICompanionController` | +| Windows | ✅ `IMeetingAICompanionController` | +| macOS | ✅ `ZoomSDKMeetingAICompanionController` | + +## AI Companion Features + +| Feature | Constant | Description | +|---------|----------|-------------| +| Query | `QUERY` | Ask AI Companion questions during meeting | +| Smart Summary | `SMART_SUMMARY` | Auto-generate meeting summary | +| Smart Recording | `SMART_RECORDING` | Highlight key moments in recording | + +## Android + +```java +import us.zoom.sdk.*; + +// Get AI Companion controller +InMeetingService inMeetingService = ZoomSDK.getInstance().getInMeetingService(); +InMeetingAICompanionController aiController = inMeetingService.getInMeetingAICompanionController(); + +// Check if AI Companion is enabled for this meeting +boolean isEnabled = aiController.isAICompanionEnabled(); +Log.d("AICompanion", "Enabled: " + isEnabled); + +// Get available features +List features = aiController.getAvailableFeatures(); +for (AICompanionFeature feature : features) { + Log.d("AICompanion", "Feature available: " + feature.name()); +} + +// Check specific feature +boolean hasSummary = aiController.isFeatureAvailable(AICompanionFeature.SMART_SUMMARY); +``` + +### AI Companion Events (Android) + +```java +// Listen for AI Companion status changes +inMeetingService.addListener(new InMeetingServiceListener() { + @Override + public void onAICompanionActiveChanged(boolean isActive) { + Log.d("AICompanion", "Active state changed: " + isActive); + } + + @Override + public void onAICompanionFeaturesChanged() { + // Re-check available features + List features = aiController.getAvailableFeatures(); + } +}); +``` + +## iOS (Swift) + +```swift +import MobileRTC + +// Get AI Companion controller +guard let meetingService = MobileRTC.shared().getMeetingService(), + let aiController = meetingService.getInMeetingAICompanionController() else { + return +} + +// Check if AI Companion is enabled +let isEnabled = aiController.isAICompanionEnabled() +print("AI Companion enabled: \(isEnabled)") + +// Get available features +if let features = aiController.getAvailableFeatures() as? [MobileRTCAICompanionFeature] { + for feature in features { + print("Feature: \(feature.rawValue)") + } +} + +// Check specific feature +let hasSummary = aiController.isFeatureAvailable(.smartSummary) +``` + +### AI Companion Events (iOS) + +```swift +// Implement delegate +class MeetingDelegate: NSObject, MobileRTCMeetingServiceDelegate { + func onAICompanionActiveChanged(_ isActive: Bool) { + print("AI Companion active: \(isActive)") + } + + func onAICompanionFeaturesChanged() { + // Refresh available features + } +} +``` + +## Windows (C++) + +```cpp +#include "zoom_sdk.h" + +// Get AI Companion controller +IMeetingService* meetingService = SDKInterfaceWrap::GetInst().GetMeetingService(); +IMeetingAICompanionController* aiController = meetingService->GetMeetingAICompanionController(); + +// Check status +bool isEnabled = aiController->IsAICompanionEnabled(); + +// Get features +IList* features = aiController->GetAvailableFeatures(); +for (int i = 0; i < features->GetCount(); i++) { + AICompanionFeature feature = features->GetItem(i); + // Process feature +} +``` + +## macOS (Objective-C) + +```objc +#import + +// Get AI Companion controller +ZoomSDKMeetingService *meetingService = [[ZoomSDK sharedSDK] getMeetingService]; +ZoomSDKMeetingAICompanionController *aiController = [meetingService getAICompanionController]; + +// Check status +BOOL isEnabled = [aiController isAICompanionEnabled]; +NSLog(@"AI Companion enabled: %d", isEnabled); + +// Get features +NSArray *features = [aiController getAvailableFeatures]; +for (NSNumber *feature in features) { + NSLog(@"Feature: %@", feature); +} +``` + +## Web SDK + +The Web SDK doesn't expose direct AI Companion controls. AI Companion behavior is determined by: +- Account settings +- Meeting settings +- Host controls + +```javascript +// AI Companion features are controlled by meeting/account settings +// The Web SDK respects these settings automatically + +// You can check meeting info for AI-related settings +ZoomMtg.getCurrentMeetingInfo(function(result) { + console.log('Meeting info:', result); +}); +``` + +## Use Cases + +### Display AI Companion Status + +```java +// Android: Show UI indicator based on AI Companion status +public void updateAICompanionUI() { + InMeetingAICompanionController aiController = getAIController(); + + if (aiController.isAICompanionEnabled()) { + aiIndicator.setVisibility(View.VISIBLE); + + if (aiController.isFeatureAvailable(AICompanionFeature.SMART_SUMMARY)) { + summaryBadge.setVisibility(View.VISIBLE); + } + } else { + aiIndicator.setVisibility(View.GONE); + } +} +``` + +### Inform Users About AI Features + +```swift +// iOS: Show alert about AI features +func showAICompanionInfo() { + guard let aiController = getAIController() else { return } + + var features: [String] = [] + + if aiController.isFeatureAvailable(.smartSummary) { + features.append("Meeting Summary") + } + if aiController.isFeatureAvailable(.smartRecording) { + features.append("Smart Recording") + } + + if !features.isEmpty { + let message = "AI Companion features active: \(features.joined(separator: ", "))" + showAlert(title: "AI Companion", message: message) + } +} +``` + +## Limitations + +| Limitation | Notes | +|------------|-------| +| No bot support | Meeting SDK is for human use only | +| Read-only | Cannot enable/disable features via SDK | +| Settings-dependent | Features depend on account/meeting settings | +| No transcript access | Use REST API or RTMS for transcripts | + +## Related + +- **[AI Companion Integration Use Case](../../general/use-cases/ai-companion-integration.md)** - Full integration guide +- **rtms** - For real-time transcript access +- **zoom-rest-api** - For meeting summaries and transcripts after meeting diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/android.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/android.md new file mode 100644 index 00000000..072da674 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/android.md @@ -0,0 +1,19 @@ +# Meeting SDK (Android) Pointers + +This file provides a stable link target for Android Meeting SDK questions. + +Start here: +- [../android/SKILL.md](../android/SKILL.md) + +## Common Forum Questions + +- How do I integrate the SDK with Gradle and resolve dependency conflicts? +- How do I implement custom UI (vs default UI)? +- How do I get audio/video/raw data access? + +## Practical Guidance + +- Verify you can join a meeting with default UI first. +- Collect logs and the specific error code (many issues are version or parameter mismatches). +- For "Invalid signature" questions: use `signature-playbook.md`. +- Use [../android/references/android-reference-map.md](../android/references/android-reference-map.md) for interface discovery and version drift checks. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/authorization.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/authorization.md new file mode 100644 index 00000000..4da0d9bb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/authorization.md @@ -0,0 +1,89 @@ +# Meeting SDK - Authorization + +Generate JWT signatures for Meeting SDK authentication. + +## Overview + +Meeting SDK uses JWT (JSON Web Token) signatures to authenticate users joining meetings. Signatures must be generated server-side to protect your SDK Secret. + +## Prerequisites + +- Meeting SDK Key and Secret from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- Server-side code to generate signatures + +## JWT Structure + +| Claim | Description | +|-------|-------------| +| `sdkKey` | Your SDK Key | +| `mn` | Meeting number | +| `role` | 0 = participant, 1 = host | +| `iat` | Issued at timestamp | +| `exp` | Expiration timestamp | +| `tokenExp` | Token expiration timestamp | + +## Signature Generation Best Practices + +### Short-Lived Tokens (Recommended) + +For security, generate tokens with short expiry: + +```javascript +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours in the past +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + +const payload = { + sdkKey: SDK_KEY, + mn: meetingNumber, + role: role, + iat: iat, + exp: exp, + tokenExp: exp +}; +``` + +**Why this works:** +- `exp` is only 10 seconds after generation (short-lived for security) +- `iat` is set 2 hours in the past to satisfy Zoom's requirement that `exp - iat >= 2 hours` +- Token is generated just before joining, so 10 second window is sufficient + +### Server-Side Example (Node.js) + +```javascript +const jwt = require('jsonwebtoken'); + +function generateSignature(sdkKey, sdkSecret, meetingNumber, role) { + const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago + const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + + const payload = { + sdkKey: sdkKey, + mn: meetingNumber, + role: role, + iat: iat, + exp: exp, + tokenExp: exp + }; + + return jwt.sign(payload, sdkSecret, { algorithm: 'HS256' }); +} +``` + +## Role Values + +| Role | Value | Description | +|------|-------|-------------| +| Participant | 0 | Join as attendee | +| Host | 1 | Join as host (requires host key or being meeting owner) | + +## Security Guidelines + +| Do | Don't | +|----|-------| +| Generate signatures server-side | Expose SDK Secret in client code | +| Use short expiry times | Use long-lived tokens | +| Validate user before generating | Generate for unauthenticated users | + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/meeting-sdk/auth/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/bot-authentication.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/bot-authentication.md new file mode 100644 index 00000000..7ed7d426 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/bot-authentication.md @@ -0,0 +1,385 @@ +# Bot Authentication - JWT, ZAK, and OBF Tokens + +Understanding the different token types for Meeting SDK authentication, especially for bots joining meetings. + +## Overview + +Meeting SDK authentication involves multiple token types that serve different purposes. This guide clarifies the confusion between JWT signatures, ZAK tokens, and OBF tokens. + +## Token Types Summary + +| Token | Purpose | Always Required? | Deprecated? | +|-------|---------|------------------|-------------| +| **JWT Signature** | Initialize/authenticate Meeting SDK | **Yes** | **No** | +| **ZAK Token** | Authenticate as a Zoom user | No (situational) | **No** | +| **OBF Token** | Join external meetings with user attribution | No (required Feb 2026) | **No** | + +## Common Confusion + +### JWT App Type vs JWT Signature + +**This is the #1 source of confusion.** + +| Term | What It Is | Status | +|------|------------|--------| +| **JWT App Type** | A Zoom app type for REST API authentication | **Deprecated** (migrated to Server-to-Server OAuth) | +| **JWT Signature** | A token generated using SDK credentials to authenticate Meeting SDK | **Still required and NOT deprecated** | + +**Key Point:** The deprecation of JWT App Type does NOT affect Meeting SDK. You still need JWT signatures for SDK authentication. + +--- + +## 1. JWT Signature (Always Required) + +### What Is It? + +A JWT (JSON Web Token) signature authenticates your application to use the Meeting SDK. Generated server-side using your SDK Client ID and Client Secret. + +### When to Use + +**Always.** Every Meeting SDK join requires a JWT signature. + +### How to Generate + +```javascript +// Server-side (Node.js) +const KJUR = require('jsrsasign'); + +function generateSignature(sdkKey, sdkSecret, meetingNumber, role) { + const iat = Math.round(Date.now() / 1000) - 30; // 30 seconds ago + const exp = iat + 60 * 60 * 2; // 2 hours from iat + + const payload = { + sdkKey: sdkKey, // Your SDK Client ID + mn: meetingNumber, // Meeting number to join + role: role, // 0 = participant, 1 = host + iat: iat, + exp: exp, + tokenExp: exp + }; + + const header = { alg: 'HS256', typ: 'JWT' }; + + return KJUR.jws.JWS.sign('HS256', + JSON.stringify(header), + JSON.stringify(payload), + sdkSecret // Your SDK Client Secret + ); +} +``` + +### Best Practice: Short-Lived Tokens + +```javascript +// Generate token just before joining +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours in past +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + +// Why this works: +// - exp is short-lived (security) +// - exp - iat >= 2 hours (Zoom requirement) +// - Token generated right before use +``` + +### Role Values + +| Role | Value | Description | +|------|-------|-------------| +| Participant | 0 | Join as attendee | +| Host | 1 | Join as host (requires being meeting owner or having host key) | + +--- + +## 2. ZAK Token (Zoom Access Key) + +### What Is It? + +A short-lived credential that proves your bot/app is authenticated as a specific Zoom user. Generated via the Zoom REST API. + +### When to Use + +| Scenario | ZAK Required? | +|----------|---------------| +| Meeting has "Only Authenticated Users Can Join" enabled | **Yes** | +| Starting a meeting as the host (when host not present) | **Yes** (host's ZAK) | +| Changing bot's profile picture to match a user | **Yes** | +| Regular meeting join | No | + +### How to Get ZAK Token + +**Step 1: Get OAuth Access Token** + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic {BASE64(client_id:client_secret)}" \ + -d "grant_type=authorization_code&code={auth_code}&redirect_uri={redirect_uri}" +``` + +**Step 2: Generate ZAK Token** + +```bash +curl -X GET "https://api.zoom.us/v2/users/me/token?type=zak&ttl=7200" \ + -H "Authorization: Bearer {access_token}" +``` + +**Response:** +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." +} +``` + +### Required OAuth Scope + +``` +user:read:zak +``` + +### Using ZAK in SDK Join + +```javascript +// Web SDK +ZoomMtg.join({ + signature: signature, // JWT signature (always required) + sdkKey: clientId, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + zak: zakToken, // ZAK token for authenticated join + success: (success) => console.log('Joined'), + error: (error) => console.error(error) +}); +``` + +### Key Properties + +- **Short-lived**: Configurable TTL (typically 1-2 hours) +- **Any ZAK works**: Doesn't need to be from a meeting participant +- **No concurrency limit**: One service account can generate unlimited tokens +- **Expiry checked at join**: If already in meeting, bot stays connected even if ZAK expires + +### Common Mistake + +**Wrong:** Thinking ZAK must be from a meeting participant. + +**Right:** Any Zoom account's ZAK satisfies "Only Authenticated Users" requirement. Create one service account (e.g., `meeting-bot@company.com`) for all your bots. + +--- + +## 3. OBF Token (On-Behalf-Of Token) + +### What Is It? + +A new credential that ties your bot to a specific user who is present in the meeting. Required for external meetings starting **February 23, 2026**. + +### Why Introduced? + +Zoom introduced OBF tokens for accountability and transparency. It makes clear that an SDK app belongs to a specific person in the meeting. + +### When to Use + +| Date | External Meeting Requirement | +|------|------------------------------| +| Before Feb 23, 2026 | ZAK or OBF (optional) | +| **After Feb 23, 2026** | **ZAK or OBF required** | + +### Critical Difference: OBF vs ZAK + +| Aspect | ZAK Token | OBF Token | +|--------|-----------|-----------| +| User presence required | No | **Yes** - user MUST be in meeting | +| Bot disconnection | Stays if token owner leaves | **Immediately disconnected** if user leaves | +| Meeting scope | Any meeting | Specific meeting ID only | +| Attribution | Generic authentication | Tied to specific attending user | + +### How to Get OBF Token + +**Step 1: Get OAuth Access Token** (same as ZAK) + +**Step 2: Generate OBF Token** + +```bash +curl -X GET "https://api.zoom.us/v2/users/me/token?type=onbehalf&meeting_id={meeting_id}" \ + -H "Authorization: Bearer {access_token}" +``` + +**Response:** +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." +} +``` + +### Required OAuth Scope + +``` +user:read:token +``` + +### Using OBF in SDK Join + +```javascript +// Web SDK +ZoomMtg.join({ + signature: signature, // JWT signature (always required) + sdkKey: clientId, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + obfToken: obfToken, // OBF token (NOT zak) + success: (success) => console.log('Joined'), + error: (error) => console.error(error) +}); +``` + +**Important:** `zak` and `obfToken` are **mutually exclusive**. Use only one. + +### Handling Join Failures + +If bot joins before authorizing user is in meeting: + +```javascript +// SDK v6.6.10+ returns specific error code +// MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING + +async function joinWithRetry(joinOptions, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await ZoomMtg.join(joinOptions); + return; // Success + } catch (error) { + if (error.code === 'MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING') { + console.log(`User not in meeting yet. Retry ${i + 1}/${maxRetries}`); + await sleep(3000); // Wait 3 seconds + } else { + throw error; // Different error, don't retry + } + } + } + throw new Error('Max retries exceeded - user never joined meeting'); +} +``` + +### OBF Token Mapping + +You must map users to their meetings: + +1. **Zoom Meetings API**: `GET /users/{userId}/meetings` +2. **Calendar integration**: Parse meeting invites from Google Calendar/Outlook + +--- + +## Complete Bot Join Flow + +### Current (Pre-Feb 2026) + +```javascript +// 1. Generate JWT signature (always required) +const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); + +// 2. Optionally get ZAK for authenticated-only meetings +let zakToken = null; +if (meetingRequiresAuth) { + zakToken = await getZAKToken(accessToken); +} + +// 3. Join meeting +await ZoomMtg.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + zak: zakToken, // Optional +}); +``` + +### Post-Feb 2026 (External Meetings) + +```javascript +// 1. Generate JWT signature (always required) +const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); + +// 2. For external meetings, get OBF token +const obfToken = await getOBFToken(accessToken, meetingNumber); + +// 3. Wait for authorizing user to join (if using OBF) +// ... implement retry logic ... + +// 4. Join meeting +await ZoomMtg.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + obfToken: obfToken, // For external meetings +}); +``` + +--- + +## Linux Bot Implementation + +For headless Linux bots, use the official sample: + +```bash +# Clone sample repository +git clone git@github.com:zoom/meetingsdk-headless-linux-sample.git + +# Configure (sample.config.toml) +[credentials] +client_id = "YOUR_CLIENT_ID" +client_secret = "YOUR_CLIENT_SECRET" + +[meeting] +join_url = "https://zoom.us/j/123456789?pwd=xxx" +# OR +meeting_id = 123456789 +password = "abc123" + +# Optional tokens +zak_token = "..." # For authenticated joins +obf_token = "..." # For external meetings + +# Run with Docker +docker compose up +``` + +--- + +## Common Mistakes + +| Mistake | Reality | Fix | +|---------|---------|-----| +| JWT signatures are deprecated | Only JWT App Type is deprecated | Continue using JWT signatures for SDK | +| ZAK must be from meeting participant | Any Zoom account's ZAK works | Use single service account | +| Using both ZAK and OBF together | They're mutually exclusive | Use only one | +| Generating OBF before user in meeting | OBF requires user presence | Implement retry logic | +| Development credentials for external meetings | Dev credentials only work for your account | Get production credentials (4-6 week review) | + +--- + +## Timeline + +| Date | Change | +|------|--------| +| **Now** | JWT signatures required; ZAK optional | +| **Nov 2025** | SDK v6.6.10 with OBF-specific error codes | +| **Feb 23, 2026** | **OBF or ZAK required for external meetings** | + +--- + +## OAuth Scopes Summary + +| Token | Required Scope | +|-------|----------------| +| ZAK Token | `user:read:zak` | +| OBF Token | `user:read:token` | + +## Resources + +- **Meeting SDK Auth**: https://developers.zoom.us/docs/meeting-sdk/auth/ +- **OBF Token Announcement**: https://developers.zoom.us/blog/transition-to-obf-token-meetingsdk-apps/ +- **Linux SDK Sample**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/breakout-rooms.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/breakout-rooms.md new file mode 100644 index 00000000..c8560823 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/breakout-rooms.md @@ -0,0 +1,490 @@ +# Breakout Rooms + +Programmatically manage breakout rooms in Zoom meetings across all platforms. + +## Overview + +Breakout rooms allow hosts to split meeting participants into smaller groups. This guide covers SDK APIs for creating, managing, and controlling breakout rooms. + +## Platform Support + +| Platform | Support Level | Notes | +|----------|---------------|-------| +| **Web SDK** | Full | Complete API | +| **iOS SDK** | Full | Creator + Admin helpers | +| **Android SDK** | Full | Creator + Admin helpers | +| **Windows SDK** | Full | Controller interface | +| **macOS SDK** | Full | Controller interface | +| **Linux SDK** | Limited | Basic functionality only | +| **Video SDK** | Different | Uses "Subsessions" - not native breakout rooms | + +**Important:** Video SDK does NOT have native breakout rooms. It uses a "Subsessions" concept requiring manual session management. + +## REST API: Pre-assigned Rooms + +Create meetings with pre-assigned breakout rooms: + +```bash +POST /v2/users/{userId}/meetings +``` + +```json +{ + "topic": "Team Workshop", + "type": 2, + "settings": { + "breakout_room": { + "enable": true, + "rooms": [ + { + "name": "Team Alpha", + "participants": ["user1@example.com", "user2@example.com"] + }, + { + "name": "Team Beta", + "participants": ["user3@example.com", "user4@example.com"] + } + ] + } + } +} +``` + +**Limitation:** Pre-assigned rooms are NOT auto-opened. The host must manually open breakout rooms when the meeting starts. There is NO REST API to auto-open rooms. + +--- + +## Web SDK Implementation + +### Create Breakout Rooms + +```javascript +// Create 5 rooms with auto-generated names (Room 1, Room 2, etc.) +ZoomMtg.BreakoutRoom.createBreakoutRoom({ + data: 5, + success: (response) => console.log('Rooms created:', response), + error: (error) => console.error('Error:', error) +}); + +// Create named rooms +ZoomMtg.BreakoutRoom.createBreakoutRoom({ + data: [ + { name: 'Engineering' }, + { name: 'Design' }, + { name: 'Product' } + ], + success: (response) => console.log('Rooms created:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Get Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.getBreakoutRooms({ + success: (response) => { + const rooms = response.result.rooms; + rooms.forEach(room => { + console.log(`Room ID: ${room.boId}, Name: ${room.name}`); + }); + }, + error: (error) => console.error('Error:', error) +}); +``` + +### Assign Participants + +```javascript +// Get unassigned attendees first +ZoomMtg.BreakoutRoom.getUnassignedAttendeeList({ + success: (response) => { + const unassigned = response.result.unassignedAttendeeList; + console.log('Unassigned:', unassigned); + } +}); + +// Assign user to a room +ZoomMtg.BreakoutRoom.assignUserToBreakoutRoom({ + targetRoomId: 'room-id-here', + userId: 12345678, + success: (response) => console.log('Assigned:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Move Participants Between Rooms + +```javascript +ZoomMtg.BreakoutRoom.moveUserToBreakoutRoom({ + targetRoomId: 'destination-room-id', + userId: 12345678, + success: (response) => console.log('Moved:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Open Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.openBreakoutRooms({ + options: { + isAutoJoinRoom: false, // Let participants choose + isBackToMainSessionEnabled: true, // Allow returning to main + isTimerEnabled: true, // Enable countdown + timerDuration: 1800, // 30 minutes (seconds) + needCountDown: true, // Show countdown + waitSeconds: 60 // Wait before auto-join + }, + success: (response) => console.log('Rooms opened:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Close Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.closeBreakoutRooms({ + success: (response) => console.log('Rooms closed:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Broadcast Message + +```javascript +ZoomMtg.BreakoutRoom.broadcast({ + message: 'Please return to the main room in 2 minutes', + success: (response) => console.log('Broadcast sent:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Check User Status + +```javascript +// Get current user's breakout room +ZoomMtg.BreakoutRoom.getCurrentBreakoutRoom({ + success: (response) => { + const { roomId, name, attendeeStatus } = response.result; + console.log(`Current room: ${name}, Status: ${attendeeStatus}`); + } +}); + +// attendeeStatus values: +// 1: UNASSIGNED - Not assigned to any room +// 2: ASSIGNED_NOT_JOIN - Assigned but hasn't joined yet +// 3: IN_BO - Currently in breakout room +``` + +--- + +## iOS SDK Implementation + +### Get Helpers + +```objc +#import + +// Get meeting service +MobileRTCMeetingService *meetingService = [[MobileRTC sharedRTC] getMeetingService]; + +// Get breakout room creator (for creating rooms) +MobileRTCBOCreator *boCreator = [meetingService getCreatorHelper]; + +// Get breakout room admin (for managing rooms) +MobileRTCBOAdmin *boAdmin = [meetingService getAdminHelper]; +``` + +### Create Rooms + +```objc +// Create 3 breakout rooms +[boCreator createBreakoutRoom:3 completion:^(NSError *error) { + if (error) { + NSLog(@"Error: %@", error.localizedDescription); + } else { + NSLog(@"Rooms created"); + } +}]; + +// Create room with specific name +[boCreator createBreakoutRoomWithName:@"Engineering" completion:^(NSError *error) { + // Handle result +}]; +``` + +### Manage Rooms + +```objc +// Open all rooms +[boAdmin openAllRoomsCompletion:^(NSError *error) { + if (!error) { + NSLog(@"Rooms opened"); + } +}]; + +// Assign user to room +[boAdmin assignUser:userId toRoom:roomId completion:^(NSError *error) { + if (!error) { + NSLog(@"User assigned"); + } +}]; + +// Close all rooms +[boAdmin closeAllRoomsCompletion:^(NSError *error) { + if (!error) { + NSLog(@"Rooms closed"); + } +}]; +``` + +### Event Handling + +```objc +@interface MyDelegate : NSObject +@end + +@implementation MyDelegate + +- (void)onMeetingBreakoutRoomStatusChanged:(MobileRTCBreakoutRoomStatus)status { + switch (status) { + case MobileRTCBreakoutRoomStatusNotStarted: + NSLog(@"Breakout rooms not started"); + break; + case MobileRTCBreakoutRoomStatusStarted: + NSLog(@"Breakout rooms started"); + break; + case MobileRTCBreakoutRoomStatusClosed: + NSLog(@"Breakout rooms closed"); + break; + } +} + +@end +``` + +--- + +## Android SDK Implementation + +### Get Helpers + +```kotlin +import us.zoom.sdk.ZoomSDK + +val zoomSDK = ZoomSDK.getInstance() +val meetingService = zoomSDK.meetingService +val boController = meetingService?.inMeetingBreakoutRoomController + +// Get creator (for creating rooms) +val creator = boController?.getCreatorHelper() + +// Get admin (for managing rooms) +val admin = boController?.getAdminHelper() +``` + +### Create Rooms + +```kotlin +// Create breakout rooms +val error = creator?.createBreakoutRoom(5) // Create 5 rooms +if (error == SDKError.SDKERR_SUCCESS) { + Log.d("Breakout", "Rooms created") +} + +// Create room with name +creator?.createBreakoutRoomWithName("Engineering") +``` + +### Manage Rooms + +```kotlin +// Open all rooms +admin?.openAllRooms() + +// Assign user to room +admin?.assignUser(userId, roomId) + +// Move user between rooms +admin?.assignUser(userId, newRoomId) // Removes from old room + +// Broadcast message +admin?.broadcastToAll("Please return in 2 minutes") + +// Close all rooms +admin?.closeAllRooms() +``` + +--- + +## Windows/macOS SDK Implementation + +### Windows (C++) + +```cpp +#include "meeting_breakout_rooms_interface.h" + +class MyBreakoutRoomsEvent : public IMeetingBreakoutRoomsEvent { +public: + void OnBreakoutRoomsStartedNotification(const wchar_t* stBID) override { + // Handle breakout rooms started + wprintf(L"Breakout rooms started: %s\n", stBID); + } +}; + +// Get controller +IMeetingBreakoutRoomsController* pController = + pMeetingService->GetBreakoutRoomsController(nullptr); + +// Set event handler +pController->SetEvent(new MyBreakoutRoomsEvent()); + +// Get list of rooms +IList* pRoomList = pController->GetBreakoutRoomsInfoList(); +for (int i = 0; i < pRoomList->GetItemCount(); ++i) { + IBreakoutRoomsInfo* pRoom = pRoomList->GetItem(i); + wprintf(L"Room: %s (ID: %s)\n", + pRoom->GetBreakoutRoomName(), + pRoom->GetBID()); +} + +// Join a breakout room +pController->JoinBreakoutRoom(L"room-id"); + +// Leave breakout room +pController->LeaveBreakoutRoom(); +``` + +### macOS (Objective-C) + +```objc +#import + +// Get controller +ZoomSDKBreakoutRoomsController *boController = + [[ZoomSDK sharedSDK] getMeetingService] getBreakoutRoomsController]; + +// Join breakout room +[boController requestJoinBreakoutRoom:@"room-id"]; + +// Leave breakout room +[boController requestLeaveBreakoutRoom]; + +// Close all rooms (host only) +[boController requestCloseAllBreakoutRooms]; +``` + +--- + +## Permissions + +### Host/Co-Host Restrictions + +| Action | Host | Co-Host | Participant | +|--------|------|---------|-------------| +| Create breakout rooms | ✅ | ✅ | ❌ | +| Open breakout rooms | ✅ | ✅ | ❌ | +| Close breakout rooms | ✅ | ✅ | ❌ | +| Assign participants | ✅ | ✅ | ❌ | +| Move participants | ✅ | ❌ | ❌ | +| Broadcast messages | ✅ | ✅ | ❌ | +| Join any room | ✅ | ✅* | ❌ | + +*Co-hosts can only join rooms assigned by host. + +--- + +## Limitations + +### Capacity Limits + +| Account Type | Max Rooms | Max Participants | +|--------------|-----------|------------------| +| Standard | 50 rooms | 500 total | +| Large Meeting Add-on | 100 rooms | 1,000 total | + +### Recording Limitations + +- **Cloud Recording**: Only records the **main session** +- **Local Recording**: Records only the room the recorder is in +- **Host cannot record** breakout rooms they're not in + +### Cannot Auto-Open Pre-Assigned Rooms + +**Critical:** There is NO API to auto-open pre-assigned breakout rooms. The host MUST manually open rooms when the meeting starts. + +### Session Timeout + +If no participant remains in the main session during breakout rooms, the main session may close after timeout. Ensure at least one participant (host or bot) stays in main session. + +--- + +## Best Practices + +### 1. Check Support Before Creating + +```javascript +ZoomMtg.BreakoutRoom.getBreakoutRoomOptions({ + success: (response) => { + if (response.result.isSupportBreakoutRoom) { + // Proceed to create rooms + } + } +}); +``` + +### 2. Handle User Status + +```javascript +// Check before assigning +ZoomMtg.BreakoutRoom.getUserStatus({ + userId: userId, + success: (response) => { + const { attendeeStatus } = response.result; + if (attendeeStatus === 3) { // IN_BO + // User already in a room - move instead of assign + } + } +}); +``` + +### 3. Error Handling + +```javascript +function handleBreakoutError(error) { + switch (error.method) { + case 'createBreakoutRoom': + if (error.errorMessage.includes('not support')) { + alert('Breakout rooms not enabled for this meeting'); + } + break; + case 'assignUserToBreakoutRoom': + if (error.errorMessage.includes('not host')) { + alert('Only host/co-host can assign participants'); + } + break; + } +} +``` + +--- + +## Video SDK Note + +Video SDK does **NOT** have native breakout rooms. Instead, use "Subsessions": + +1. Create separate Video SDK sessions +2. Move participants between sessions programmatically +3. Implement your own room management logic + +See: https://developers.zoom.us/blog/build-breakout-rooms-for-video-sdk/ + +--- + +## Resources + +- **Web SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **iOS SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/ios/ +- **Android SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/android/ +- **REST API - Meetings**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/environment-variables.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/environment-variables.md new file mode 100644 index 00000000..a56bc589 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/environment-variables.md @@ -0,0 +1,29 @@ +# Zoom Meeting SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | Meeting SDK signature issuer identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Signature generation secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Per flow | Meeting to join/start | Zoom meeting invite, Zoom web portal, or Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Meeting invite details / Meetings API | +| `ZOOM_ROLE` | Conditional | Signature role (`0` attendee, `1` host) | Set by your app logic | +| `ZOOM_ZAK` | Host flows only | Host authorization token for start flows | Generate via Zoom REST API user token endpoint | + +## Runtime-only values + +- `MEETING_SDK_JWT` (generated signature) + +Generate server-side and keep short-lived. + +## Notes + +- Never expose `ZOOM_SDK_SECRET` in frontend/mobile clients. + +## Platform-Specific References + +- Android: [../android/references/environment-variables.md](../android/references/environment-variables.md) +- iOS: [../ios/references/environment-variables.md](../ios/references/environment-variables.md) +- macOS: [../macos/references/environment-variables.md](../macos/references/environment-variables.md) +- Unreal: [../unreal/references/environment-variables.md](../unreal/references/environment-variables.md) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/forum-top-questions.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/forum-top-questions.md new file mode 100644 index 00000000..efe16b4d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/forum-top-questions.md @@ -0,0 +1,100 @@ +--- +title: "Forum-Derived Top Questions (Meeting SDK)" +--- + +# Forum-Derived Top Questions (Meeting SDK) + +Use this as a high-signal checklist of what developers repeatedly ask about the **Zoom Meeting SDK** (Web, Mobile, Desktop, Linux). + +## Fast Routing Questions (Ask First) + +- Platform: **Web** vs **Android/iOS** vs **Windows/macOS** vs **Linux headless** +- Web integration: **Client View (CDN + `ZoomMtg`)** vs **Component View (npm + `ZoomMtgEmbedded`)** +- Join type: **join** as participant vs **start** as host +- Auth inputs you have: `sdkKey`, `sdkSecret` (server only), `meetingNumber`, `role`, `zak` (if starting as host), `passcode` +- Exact error: full error code/message + SDK version + +## Signatures (Most Common Root Cause) + +- **Generate signature server-side only** (never expose SDK Secret in browser/mobile client code). +- Use the right payload fields: + - `sdkKey`, `mn` (meeting number), `role`, `iat`, `exp`, `tokenExp` +- Typical mistakes: + - Wrong meeting number format (non-digits; strip formatting) + - `exp` too long/short, or client/server clock skew + - Mixing Meeting SDK signature with REST API OAuth/JWT app-type tokens (different things) + +## Web SDK: Client View vs Component View Confusion + +- **Client View (CDN)** uses `ZoomMtg.*` callback style. +- **Component View (npm)** uses `ZoomMtgEmbedded.createClient()` with promise-based APIs. +- When a question is about “hide UI”, “toolbar”, “meeting info”, first confirm which view they are using: + - Some UI changes are only possible in **one** of the views, or not supported at all. + +## “Hide Meeting Password / Invite URL / Meeting Info” + +Common ask: “How do I hide passcode / meeting info / invite URL in Meeting SDK?” + +What to cover in answers: +- What’s supported by the SDK (official flags/APIs) vs what is not. +- If the goal is “don’t leak passcode”, the most reliable approach is usually: + - Use meeting settings that reduce exposure (and avoid displaying it in your own UI) + - Don’t log it client-side + - Don’t render invite UI if you control that surface (Component View) + +## Join Failures and Timeouts (Web) + +Common asks: +- “Join meeting failed” +- “Joining meeting timeout” +- “Browser doesn’t support gallery view” + +Checklist: +- Confirm `crossOriginIsolated` / SharedArrayBuffer requirements (if using features that need it) +- Confirm HTTPS + correct COOP/COEP headers (when required) +- Confirm ad blockers / CSP / corporate proxies aren’t blocking Zoom assets +- Confirm correct `passWord` casing (Web Client View uses `passWord`) + +## Captions / Transcript UI + +Common asks: +- Enabling captions +- Hiding captions but transcript still shows + +Answer pattern: +- Separate “what the host/account policy controls” from “what SDK UI controls” +- Call out constraints: some transcript/caption behaviors are server-side policy and not fully suppressible from SDK UI alone + +## Waiting Room and Admission Flow + +Common asks: +- Enabling/disabling waiting room +- Notifications and UI behavior + +Answer pattern: +- Distinguish Meeting settings (host/account) vs SDK client behavior +- If the goal is “auto-admit” or “control admission”, you likely need the host controls (and sometimes REST API) rather than just SDK UI changes + +## Raw Data / Raw Recording + +Common asks: +- Start raw recording fails (permissions) +- Raw data availability varies by platform + +Answer pattern: +- Always ask platform + SDK variant and whether they’re using supported raw-data APIs for that platform +- If it’s permission-related: + - confirm required entitlements/features + - confirm app permissions / OS permissions + +## Performance: “Save CPU”, Gallery View, Low-End Devices + +Common asks: +- Gallery view availability +- High CPU usage + +Checklist: +- Reduce subscribed video streams / lower quality where supported +- Ensure you’re not rendering unnecessary DOM/video elements (Component View) +- Confirm the device/browser constraints (some behavior is expected) + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/ios.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/ios.md new file mode 100644 index 00000000..6149a7d0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/ios.md @@ -0,0 +1,27 @@ +# Meeting SDK (iOS) Pointers + +Use this page as a quick starting point for iOS Meeting SDK questions before diving into the deeper iOS references. + +Start here: +- [../ios/SKILL.md](../ios/SKILL.md) + +## Common Forum Questions + +- How do I initialize the iOS Meeting SDK? +- What permissions/entitlements do I need for camera/mic? +- How do I customize UI vs default UI? + +## Practical Guidance + +- Start with default UI until basic join/start works. +- Confirm camera/mic permission flows. +- When debugging join failures, capture: + - SDK version + - init/join return codes + - meeting number vs meeting UUID confusion +- Use [../ios/references/ios-reference-map.md](../ios/references/ios-reference-map.md) to confirm current API surface before assuming wrapper/sample parity. + +## Next + +- `android.md` for Android equivalents. +- `authorization.md` and `signature-playbook.md` for auth/signature concepts shared across platforms. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/macos.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/macos.md new file mode 100644 index 00000000..dc70aa4d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/macos.md @@ -0,0 +1,16 @@ +# Meeting SDK (macOS) Pointers + +Stable pointer for macOS Meeting SDK questions. + +Start here: +- [../macos/SKILL.md](../macos/SKILL.md) + +Common question patterns: +- How do I choose default UI vs custom UI? +- How do I handle start/join flows with host privileges? +- Which service/controller delegates must be registered first? + +Practical guidance: +- Validate default UI join path before custom UI. +- Confirm auth/signature and role alignment before host actions. +- Use [../macos/references/macos-reference-map.md](../macos/references/macos-reference-map.md) for API discovery and upgrade drift checks. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/multiple-meetings.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/multiple-meetings.md new file mode 100644 index 00000000..dd103e2e --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/multiple-meetings.md @@ -0,0 +1,28 @@ +--- +title: "Multiple Meetings and Multiple Instances" +--- + +# Multiple Meetings and Multiple Instances + +Common forum question: “How do I attend two meeting rooms at the same time?” + +## Core Constraint + +In most SDK/client integrations, a single Meeting SDK client instance is designed around **one active meeting session at a time**. + +If you need “two meetings at once”, you typically need **two separate instances** (and often separate process/browser contexts/devices). + +## Practical Options (What Usually Works) + +- **Two devices** (simplest operationally) +- **Two separate processes** (desktop apps) +- **Two separate browser profiles/containers** (web), if supported by the environment + +## What to Clarify + +- Platform (Web vs Windows/macOS vs Android/iOS vs Linux) +- Whether you need: + - audio/video in both meetings concurrently + - or just monitoring/viewing one while connected to another +- Any compliance constraints (recording, transcription, etc.) + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/signature-playbook.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/signature-playbook.md new file mode 100644 index 00000000..6bb13e4c --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/signature-playbook.md @@ -0,0 +1,45 @@ +--- +title: "Meeting SDK Signature Playbook" +--- + +# Meeting SDK Signature Playbook + +Most “join failed” issues reduce to signature generation or mismatched inputs. + +## Rules + +1. **Generate signatures server-side only**. Never ship the SDK Secret to the browser/app. +2. `meetingNumber` must be digits only. +3. `role` must match what you are doing: + - `0` = join as attendee + - `1` = start as host +4. Keep `iat/exp` reasonable and account for clock skew (server time matters). + +## Required Payload Fields (Common Pattern) + +You’ll typically see fields like: + +- `sdkKey` +- `mn` (meeting number) +- `role` +- `iat`, `exp`, `tokenExp` + +If the developer is mixing in REST API OAuth tokens or Marketplace JWT app-type tokens, stop and clarify: those are **not** Meeting SDK signatures. + +## Common Failure Modes + +- **Invalid signature**: + - wrong secret + - wrong `mn` format + - expired `exp/tokenExp` + - generating signature for role=1 but joining (or vice versa) +- **4003 Invalid Parameter** (common on Web “start” flows): + - role mismatch or missing host requirements (often needs ZAK for host start flows) +- **Works locally but not in prod**: + - different env vars/secret + - prod server clock skew + +## Web-Specific Gotcha: `passWord` + +On Web Client View (`ZoomMtg.join`) the key is `passWord` (capital W). If the meeting has a passcode and it’s missing/misnamed, join fails in a way that looks like auth trouble. + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/triage-intake.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/triage-intake.md new file mode 100644 index 00000000..258f31af --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/triage-intake.md @@ -0,0 +1,52 @@ +--- +title: "Meeting SDK Triage Intake (What to Ask First)" +--- + +# Meeting SDK Triage Intake (What to Ask First) + +This is the fastest way to turn a vague “Meeting SDK isn’t working” question into an answer. + +## 1) Which SDK + Which Platform? + +- Platform: `web` | `android` | `ios` | `react-native` | `electron` | `windows` | `macos` | `linux` +- Exact SDK package/version (and Zoom client version if relevant) + +## 2) Web: Client View vs Component View + +Ask explicitly which one they’re using: + +- **Client View (CDN)**: uses `ZoomMtg.*` (callbacks) +- **Component View (npm)**: uses `ZoomMtgEmbedded.createClient()` (promises) + +These have different APIs and different customization limitations. + +## 3) Join vs Start, and Role + +- Are you **joining** as an attendee (`role=0`) or **starting** as host (`role=1`)? +- If starting as host, do you have a **ZAK** (Zoom Access Key) when required by the platform/SDK flow? + +## 4) Inputs (Copy/Paste) + +Request the exact values and formats (redact secrets): + +- `meetingNumber` (digits only) +- `passcode` / `password` (and on Web Client View, confirm they used `passWord` key name) +- `userName` +- `sdkKey` (OK to share) +- signature generation code (server-side) and the payload fields used (`mn`, `role`, `iat`, `exp`, `tokenExp`) + +## 5) The Symptom + +Pick one bucket: + +- signature/auth: “Invalid signature”, “signature expired”, “invalid parameter” +- web environment: “Joining meeting timeout”, “organization disabled access”, “SharedArrayBuffer”, “gallery view” +- UI/customization: “hide meeting info”, “hide passcode/invite URL”, “remove buttons” +- media: “no audio/video”, “screen share problems”, “performance/cpu” + +## 6) Minimal Repro + Logs + +- Minimal steps to reproduce +- SDK logs (see `general/references/sdk-logs-troubleshooting.md`) +- On Web: browser + OS, console errors, and whether corporate proxy/adblock is present + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/troubleshooting.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/troubleshooting.md new file mode 100644 index 00000000..45a22c31 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/troubleshooting.md @@ -0,0 +1,132 @@ +# Meeting SDK - Troubleshooting + +Common issues and solutions for Meeting SDK. + +## Overview + +Troubleshooting guide for Meeting SDK across all platforms. + +## Common Issues + +### Join Meeting Failed + +| Error | Possible Cause | Solution | +|-------|----------------|----------| +| Invalid signature | JWT malformed or expired | Regenerate signature server-side | +| Meeting not found | Invalid meeting number | Verify meeting exists | +| Wrong password | Password mismatch | Check meeting password | +| Meeting locked | Host locked meeting | Contact host | + +**Note:** Error code 0 often means success - check the SDK enum values (e.g., `SDKERR_SUCCESS = 0`). + +### Authentication Issues + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Auth failed | Invalid credentials | Check SDK Key/Secret | +| Token expired | JWT too old | Generate fresh signature | +| Signature invalid | Wrong secret used | Verify SDK Secret | + +### No Video + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Black screen | Permission denied | Request camera permission | +| Video not starting | Camera in use | Close other camera apps | +| Poor quality | Low bandwidth | Check network | + +### No Audio + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Can't hear | Audio not connected | Join audio | +| Muted | User is muted | Check mute state | +| Echo | No echo cancellation | Use headphones | + +### Web-Specific Issues + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| SharedArrayBuffer error | Missing headers | Add COOP/COEP headers | +| Component not rendering | Wrong container | Check `zoomAppRoot` element | +| Toolbar/controls missing | Global CSS resets | Don't use `* { margin: 0; }` - scope styles to your app | +| Toolbar cropped/off-screen | Zoom UI exceeds viewport | Use `transform: scale(0.95)` on `#zmmtg-root` | +| `ZoomMtgEmbedded is undefined` | Using CDN but Component View API | CDN provides `ZoomMtg`, use npm for `ZoomMtgEmbedded` | + +### Web: "Black Screen" After Join (UI Hidden Behind App) + +If the meeting "joins" but you only see your app shell or a blank/black area, the Zoom UI may be rendered but **covered by your SPA layout**, modals, or fixed headers. + +**Applies mostly to Client View**, and occasionally to Component View if your container is mis-sized/overlaid. + +Fix (Client View): + +```css +/* Ensure the root container occupies the viewport and sits above your app shell. */ +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + z-index: 9999 !important; +} +``` + +Be critical: if you still see a "black screen", it can also be: + +- camera permission denied / no video started +- your container is `display:none` or has `height: 0` (Component View) +- global CSS resets breaking Zoom's layout + +### UI Customization Confusion (Web) + +If the question is about hiding passcode/invite URL or removing built-in controls: + +- Confirm **Client View vs Component View** +- Prefer supported customization knobs (e.g., `customize.meetingInfo` in Component View) +- Avoid brittle CSS hacks unless there's no supported alternative + +See: +- `web/references/component-view-ui-customization.md` + +### Client View CSS Fixes + +**Toolbar falling off screen:** +```css +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + transform: scale(0.95) !important; + transform-origin: top center !important; +} +``` + +**Hide your app when meeting starts:** +```css +body.meeting-active .your-app { display: none !important; } +body.meeting-active { background: #000 !important; } +``` + +## Collecting Logs + +See [SDK Logs & Troubleshooting](../../general/references/sdk-logs-troubleshooting.md) for log collection. + +## Getting Support + +1. Collect SDK logs +2. Note SDK version and platform +3. Document steps to reproduce +4. Contact [Developer Support](https://devsupport.zoom.us/) + +## Resources + +- **Developer forum**: https://devforum.zoom.us/ +- **Support**: https://devsupport.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/unreal.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/unreal.md new file mode 100644 index 00000000..6bc09a44 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/unreal.md @@ -0,0 +1,16 @@ +# Meeting SDK (Unreal) Pointers + +Stable pointer for Unreal Meeting SDK wrapper questions. + +Start here: +- [../unreal/SKILL.md](../unreal/SKILL.md) + +Common question patterns: +- Which methods are available in C++ wrapper vs Blueprint wrapper? +- Why does wrapper behavior differ from native platform SDK docs? +- How do I validate wrapper version compatibility with Unreal Engine version? + +Practical guidance: +- Treat wrapper docs as wrapper behavior source of truth first. +- Then cross-check base semantics against native Meeting SDK reference where wrapper notes indicate parity. +- Use [../unreal/references/versioning-and-compatibility.md](../unreal/references/versioning-and-compatibility.md) for known contradictions and lag risks. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/references/webinars.md b/partner-built/zoom-plugin/skills/meeting-sdk/references/webinars.md new file mode 100644 index 00000000..30913271 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/references/webinars.md @@ -0,0 +1,349 @@ +# Meeting SDK - Webinars + +Embed webinar experiences using Meeting SDK. + +## Overview + +Meeting SDK can join webinars, not just meetings. This guide covers webinar-specific features and considerations. + +## Webinar vs Meeting + +| Feature | Meeting | Webinar | +|---------|---------|---------| +| Max participants | 100-1000 | 500-50,000 | +| Participant roles | Host, co-host, participant | Host, panelist, attendee | +| Attendee video | Yes | No (view-only) | +| Attendee audio | Yes | Raise hand to unmute | +| Q&A | No | Yes | +| Registration | Optional | Common | +| Practice session | No | Yes | + +## Joining Webinars + +### Web SDK + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +// Join webinar (same as meeting) +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: webinarId, // Webinar ID + userName: 'Attendee Name', + userEmail: 'attendee@example.com', // Required for registration + passWord: password, + tk: registrantToken, // If registration required +}); +``` + +### Registration Token + +If the webinar requires registration: + +```javascript +// 1. Register via API first +const response = await axios.post( + `https://api.zoom.us/v2/webinars/${webinarId}/registrants`, + { + email: 'attendee@example.com', + first_name: 'John', + last_name: 'Doe' + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} +); + +// 2. Get registrant token +const registrantToken = response.data.registrant_id; + +// 3. Use token when joining +await client.join({ + // ... other params + tk: registrantToken, +}); +``` + +### Native SDKs (iOS/Android/Windows) + +```swift +// iOS - Join webinar +let joinParam = ZoomSDKJoinWebinarParam() +joinParam.webinarNumber = webinarNumber +joinParam.userName = "Attendee" +joinParam.userEmail = "attendee@example.com" +joinParam.webinarPassword = password +joinParam.webinarToken = registrantToken // If registered + +meetingService.joinWebinar(with: joinParam) +``` + +```kotlin +// Android - Join webinar +val joinParams = JoinMeetingParams().apply { + meetingNo = webinarId + displayName = "Attendee" + password = webinarPassword +} + +// Webinar uses same joinMeeting API +meetingService.joinMeetingWithParams(context, joinParams, JoinMeetingOptions()) +``` + +## Attendee vs Panelist + +### Role Detection + +```javascript +// Web SDK +client.on('user-updated', (payload) => { + const { userId } = payload; + const user = client.getUser(userId); + + // Check role + if (user.isHost) { + console.log('User is host'); + } else if (user.userRole === 'panelist') { + console.log('User is panelist'); + } else { + console.log('User is attendee'); + } +}); +``` + +### Attendee Limitations + +Attendees in webinars have restricted capabilities: + +| Action | Attendee Can Do? | +|--------|------------------| +| View video | Yes | +| Send video | No | +| Listen to audio | Yes | +| Unmute self | No (must be promoted) | +| Send chat | To panelists/everyone (if enabled) | +| Ask Q&A | Yes | +| Raise hand | Yes | + +## Q&A Feature + +### Web SDK Q&A + +```javascript +// Check if Q&A is enabled +const qaClient = client.getQAClient(); +const isQAEnabled = qaClient.isQAEnabled(); + +// Ask a question +await qaClient.askQuestion('What is the pricing?', false); // false = public + +// Answer a question (panelist/host only) +await qaClient.answerQuestion(questionId, 'The pricing is...'); + +// Get all questions +const questions = qaClient.getAllQuestions(); +questions.forEach(q => { + console.log(`Q: ${q.text}`); + console.log(`A: ${q.answerText || 'Unanswered'}`); +}); + +// Q&A events +client.on('question-created', (payload) => { + console.log('New question:', payload.question.text); +}); + +client.on('answer-created', (payload) => { + console.log('Answer:', payload.answer.text); +}); +``` + +### Native SDK Q&A + +```swift +// iOS +let qaController = meetingService.getWebinarQAController() + +// Ask question +qaController?.askQuestion("What is the pricing?", isAnonymous: false) + +// Get questions (host/panelist) +let questions = qaController?.getAllQuestionList() +``` + +## Raise Hand + +### Web SDK + +```javascript +// Raise hand +client.raiseHand(); + +// Lower hand +client.lowerHand(); + +// Check status +const myself = client.getCurrentUser(); +console.log('Hand raised:', myself.bRaiseHand); + +// Event +client.on('user-updated', (payload) => { + if (payload.bRaiseHand !== undefined) { + console.log(`${payload.userId} ${payload.bRaiseHand ? 'raised' : 'lowered'} hand`); + } +}); +``` + +### Native SDK + +```swift +// iOS +let webinarController = meetingService.getWebinarController() +webinarController?.raiseHand() +webinarController?.lowerHand() +``` + +## Promote/Demote Attendees + +Hosts can promote attendees to panelists: + +```javascript +// Web SDK (host only) +const webinarClient = client.getWebinarClient(); + +// Promote to panelist (allows video/audio) +await webinarClient.promoteAttendee(userId); + +// Demote back to attendee +await webinarClient.demoteAttendee(userId); + +// Allow attendee to talk (temporary unmute) +await webinarClient.allowAttendeeTalk(userId); +await webinarClient.disallowAttendeeTalk(userId); +``` + +## Polling + +### Web SDK Polling + +```javascript +// Get polling client +const pollingClient = client.getPollingClient(); + +// Check if polling available +const isAvailable = pollingClient.isPollingEnabled(); + +// Submit poll answer (attendee) +await pollingClient.submitPollAnswer(pollId, [ + { questionId: 'q1', answerIds: ['a1'] } +]); + +// Poll events +client.on('poll-started', (payload) => { + console.log('Poll started:', payload.poll); + displayPoll(payload.poll); +}); + +client.on('poll-ended', (payload) => { + console.log('Poll ended'); +}); +``` + +## Practice Session + +Before a webinar starts, hosts can run a practice session: + +```javascript +// Detect practice session +client.on('meeting-status-changed', (payload) => { + if (payload.status === 'practice') { + console.log('In practice session - attendees cannot join yet'); + } else if (payload.status === 'webinar') { + console.log('Webinar is live'); + } +}); +``` + +## Chat in Webinars + +Chat permissions differ in webinars: + +```javascript +// Get chat privilege +const chatClient = client.getChatClient(); +const privilege = chatClient.getPrivilege(); + +// privilege values: +// 1 = No one +// 2 = Host and panelists only +// 3 = Everyone publicly +// 4 = Everyone publicly and privately + +// Send to panelists (attendee) +await chatClient.send('Question about pricing', /* to all panelists */); + +// Send to everyone (if allowed) +await chatClient.sendToAll('Hello everyone!'); +``` + +## Webinar-Specific Events + +```javascript +// Web SDK events +client.on('webinar-invite', (payload) => { + // Attendee invited to become panelist +}); + +client.on('webinar-depromote', (payload) => { + // Demoted from panelist to attendee +}); + +client.on('webinar-allow-attendee-chat', (payload) => { + // Chat permissions changed +}); + +client.on('webinar-attendee-status', (payload) => { + // Attendee count updated + console.log('Attendees:', payload.attendeeCount); +}); +``` + +## Best Practices + +### For Large Webinars + +1. **Disable attendee video** - Already default for webinars +2. **Use Q&A instead of chat** - More organized for large audiences +3. **Pre-register attendees** - Better tracking and control +4. **Test with practice session** - Verify setup before going live + +### UI Considerations + +```javascript +// Adjust UI based on role +function setupUI(role) { + if (role === 'attendee') { + // Hide video controls (can't send video) + hideElement('#videoControls'); + + // Show Q&A and raise hand + showElement('#qaPanel'); + showElement('#raiseHandButton'); + + // Disable unmute (until promoted) + disableElement('#unmuteButton'); + } else if (role === 'panelist' || role === 'host') { + // Full controls + showAllControls(); + } +} +``` + +## Resources + +- **Webinar API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Webinars +- **Meeting SDK Webinar**: https://developers.zoom.us/docs/meeting-sdk/web/webinar/ +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/RUNBOOK.md new file mode 100644 index 00000000..6cdaf845 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Unreal 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Unreal (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/unreal/ +- https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/unreal/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/unreal/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/SKILL.md new file mode 100644 index 00000000..590b6baf --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/SKILL.md @@ -0,0 +1,39 @@ +--- +name: zoom-meeting-sdk-unreal +description: | + Zoom Meeting SDK for Unreal Engine wrapper integrations. Use when building Unreal projects that + embed Zoom meetings with C++ and Blueprint wrappers, including wrapper-to-SDK mapping concerns. +user-invocable: false +triggers: + - "meeting sdk unreal" + - "zoom unreal sdk" + - "unreal zoom wrapper" + - "blueprint zoom sdk" + - "unreal meeting integration" +--- + +# Zoom Meeting SDK (Unreal Engine) + +Use this skill when integrating Meeting SDK into Unreal Engine projects. + +## Start Here + +1. [unreal.md](unreal.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/unreal-reference-map.md](references/unreal-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/unreal/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/architecture.md new file mode 100644 index 00000000..7f331345 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/architecture.md @@ -0,0 +1,23 @@ +# Unreal Architecture + +## Layer Model + +- Unreal game/app layer (C++ and Blueprint graphs). +- Unreal wrapper layer (method adaptation for Blueprint/C++). +- Core Meeting SDK layer (native behavior baseline). +- Backend signature/token service. + +## Reference Flow + +```text +Unreal Gameplay/UI -> Unreal Wrapper -> Meeting SDK Core -> Zoom services + ^ | | | + | v v v + Player actions Wrapper events Native callbacks Meeting state +``` + +## Key Concept + +Always separate: +- wrapper behavior (Unreal-specific), and +- core SDK behavior (native reference semantics). diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..26018278 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md @@ -0,0 +1,14 @@ +# Unreal Lifecycle Workflow + +## Core Sequence + +1. Plugin/wrapper initialization in Unreal project startup. +2. SDK init + auth sequence (JWT/signature path). +3. Join/start flow. +4. In-meeting event handling through wrapper event interfaces. +5. Cleanup and session/resource release. + +## Wrapper-Specific Risk + +- Method availability differs between C++ wrapper and Blueprint wrapper. +- Some wrapper methods are modified or newly introduced vs native SDK method behavior. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/examples/join-start-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/examples/join-start-pattern.md new file mode 100644 index 00000000..6aeb2133 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/examples/join-start-pattern.md @@ -0,0 +1,14 @@ +# Unreal Join/Start Pattern + +## Join/Start Sequence + +1. Initialize wrapper SDK context. +2. Authenticate with backend-provided short-lived token/signature. +3. Trigger join/start through wrapper API. +4. Bind wrapper event callbacks before user-interactive meeting controls. + +## Blueprint/C++ Guardrails + +- Confirm node/function exists in selected wrapper mode. +- For modified wrapper methods, verify input/output differences from native docs. +- Keep a thin C++ adapter for shared validation logic when Blueprint nodes diverge. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/environment-variables.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/environment-variables.md new file mode 100644 index 00000000..47e657f0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/environment-variables.md @@ -0,0 +1,15 @@ +# Unreal Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep signing logic outside Unreal client. +- Treat local config values as development-only and avoid committing secrets. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/unreal-reference-map.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/unreal-reference-map.md new file mode 100644 index 00000000..824a8df6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/unreal-reference-map.md @@ -0,0 +1,23 @@ +# Unreal Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/unreal/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `6` +- API reference pages captured: `1` (single consolidated mapping page) + +## Reference Characteristics + +- Lists many wrapper interfaces and controllers. +- Explicitly documents method availability across C++ and Blueprint wrappers. +- Notes wrapper modifications/new methods relative to base Meeting SDK behavior. +- Directs developers to Windows SDK reference for unchanged base semantics. + +## Drift Signals to Watch + +- Wrapper version lag relative to latest native platform SDK versions. +- Changed Blueprint node names/signatures across Unreal wrapper releases. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md new file mode 100644 index 00000000..10f7714e --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md @@ -0,0 +1,19 @@ +# Unreal Versioning and Compatibility + +## Observed Versions + +- Local package version: `v6.1.5.43366` (`UE v5.4.3`) +- Local package naming: `zoom-meeting-sdk-unreal-engine-6.1.5-full` +- Docs baseline: current Unreal Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Validate Unreal engine version compatibility before SDK integration. +- Confirm wrapper API availability in both C++ and Blueprint paths. +- Keep a version matrix (Unreal Engine version x wrapper version x Meeting SDK behavior). + +## Contradiction/Drift Notes + +- Unreal package `CHANGELOG.md` currently points to a Windows changelog URL (packaging inconsistency). +- Wrapper docs reference base behavior from Windows SDK for unchanged methods; verify assumptions per wrapper release. +- Unreal wrapper package version is behind current mobile/desktop package streams in this workspace snapshot. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..86012780 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# Unreal High-Level Scenarios + +## Scenario 1: Virtual event experience + +- Unreal scene renders branded environment. +- Meeting SDK feeds participant media into scene surfaces. +- Hosts control sessions from Unreal UI panel. + +## Scenario 2: Industrial remote collaboration + +- Engineers join a meeting from Unreal simulation app. +- Shared scene context accompanies live meeting discussion. +- Blueprint layer drives low-code UI interactions. + +## Scenario 3: Training/education simulation + +- Learners join via Unreal experience. +- Session controls and overlays are simplified in Blueprint. +- Fallback path exists for wrapper/API differences across versions. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/troubleshooting/common-issues.md new file mode 100644 index 00000000..7d0c9a77 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/troubleshooting/common-issues.md @@ -0,0 +1,20 @@ +# Unreal Common Issues + +## 1. Wrapper method mismatch + +- Check whether method is available in C++ wrapper, Blueprint wrapper, or both. +- Validate renamed/modified Blueprint node signatures. + +## 2. Join/start behavior not matching expectations + +- Compare wrapper docs and base SDK semantics. +- Verify token/signature validity and role alignment. + +## 3. Version mismatch issues + +- Confirm Unreal engine version compatibility with package. +- Verify wrapper package version and docs are from same release family. + +## 4. Packaging contradictions + +- If changelog/reference links look inconsistent, trust runtime behavior + validated wrapper docs + controlled test matrix. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/unreal/unreal.md b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/unreal.md new file mode 100644 index 00000000..b62664bc --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/unreal/unreal.md @@ -0,0 +1,17 @@ +# Meeting SDK Unreal Guide + +## Scope + +Unreal Engine Meeting SDK wrapper integration with C++ and Blueprint exposure. + +## Validation Snapshot + +- Docs coverage includes: get-started, integrate, PKCE auth, error codes, and reference landing. +- API reference capture is a single consolidated wrapper reference page. +- Local package checked: `zoom-meeting-sdk-unreal-engine-6.1.5-full` (`UE v5.4.3`) with sample project. + +## Practical Guidance + +1. Validate wrapper version alignment before coding. +2. Confirm C++ wrapper method availability vs Blueprint wrapper availability. +3. Treat wrapper docs as mapping docs, then cross-check behavior against Meeting SDK Windows/native references for base semantics. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/RUNBOOK.md new file mode 100644 index 00000000..db5c6510 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/RUNBOOK.md @@ -0,0 +1,66 @@ +# Meeting SDK Web 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Web (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/client-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/component-view/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/SKILL.md new file mode 100644 index 00000000..baa10c0d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/SKILL.md @@ -0,0 +1,1127 @@ +--- +name: zoom-meeting-sdk-web +description: | + Zoom Meeting SDK for Web - Embed Zoom meeting capabilities into web applications. Two integration + options: Client View (full-page, familiar Zoom UI) and Component View (embeddable, Promise-based API). + Includes SharedArrayBuffer setup for HD video, gallery view, and virtual backgrounds. +user-invocable: false +triggers: + - "embed meeting web" + - "meeting in react" + - "meeting in nextjs" + - "meeting in vue" + - "meeting in angular" + - "component view" + - "client view" + - "web meeting sdk" + - "javascript meeting" + - "sharedarraybuffer" +--- + +# Zoom Meeting SDK (Web) + +Embed Zoom meeting capabilities into web applications with two integration options: **Client View** (full-page) or **Component View** (embeddable). + +## How to Implement a Custom Video User Interface for a Zoom Meeting in a Web App + +Use **Meeting SDK Web Component View**. + +Do not use Video SDK for this question unless the user is explicitly building a non-meeting session +product. + +Minimal architecture: + +```text +Browser page + -> fetch Meeting SDK signature from backend + -> ZoomMtgEmbedded.createClient() + -> client.init({ zoomAppRoot }) + -> client.join({ signature, sdkKey, meetingNumber, userName, password }) + -> apply layout/style/customize options around the embedded meeting container +``` + +Minimal implementation: + +```ts +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +export async function startEmbeddedMeeting(meetingNumber: string, userName: string, password: string) { + const sigRes = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + + if (!sigRes.ok) throw new Error(`signature_fetch_failed:${sigRes.status}`); + + const { signature, sdkKey } = await sigRes.json(); + + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement')!, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + customize: { + video: { isResizable: true, popper: { disableDraggable: false } }, + }, + }); + + await client.join({ + signature, + sdkKey, + meetingNumber, + userName, + password, + }); +} +``` + +Common failure points: +- wrong route: Video SDK instead of Meeting SDK Component View +- missing backend signature endpoint +- wrong password field (`password` here, not `passWord`) +- missing OBF/ZAK requirements for meetings outside the app account +- missing SharedArrayBuffer headers when higher-end meeting features are expected + +## Hard Routing Rule + +If the user wants a **custom video user interface for a Zoom meeting in a web app**, route to +**Component View**, not Video SDK. + +- **Meeting SDK Component View** = custom UI for a real Zoom meeting +- **Video SDK Web** = custom UI for a non-meeting video session product + +For the direct custom-meeting-UI path, start with +[component-view/SKILL.md](component-view/SKILL.md). + +## New to Web SDK? Start Here! + +**The fastest way to master the SDK:** + +1. **Choose Your View** - [Client View vs Component View](#client-view-vs-component-view) - Understand the key architectural differences +2. **Quick Start** - [Client View](#quick-start-client-view) or [Component View](#quick-start-component-view) - Get a working meeting in minutes +3. **SharedArrayBuffer** - [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) - Required for HD video, gallery view, virtual backgrounds +4. **Optional preflight diagnostics** - [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md) - Validate browser/device/network before join + +**Building a Custom Integration?** +- Component View gives you Promise-based API and embeddable UI +- Client View gives you the familiar full-page Zoom experience +- For a custom meeting UI, prefer **Component View** first +- Cross-product routing example: [../../general/use-cases/custom-meeting-ui-web.md](../../general/use-cases/custom-meeting-ui-web.md) +- [Browser Support](concepts/browser-support.md) - Feature matrix by browser +- Exact deep-dive path: [component-view/SKILL.md](component-view/SKILL.md) + +**Having issues?** +- Join errors → Check signature generation and password spelling (`passWord` vs `password`) +- HD video not working → Enable SharedArrayBuffer headers +- Complete navigation → [SKILL.md](SKILL.md) + +## Prerequisites + +- Zoom app with Meeting SDK credentials from [Marketplace](https://marketplace.zoom.us/) +- SDK Key (Client ID) and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) +- Backend auth endpoint for signature generation + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT/signature generation. +> +> **Want pre-join diagnostics?** Chain **[probe-sdk](../../probe-sdk/SKILL.md)** before `init()`/`join()` to gate low-readiness environments. + +## Optional Preflight Gate (Probe SDK) + +For unstable first-join environments, run Probe SDK checks before calling `ZoomMtg.init()` or `client.join()`: + +1. Run Probe permissions/device/network diagnostics. +2. Apply readiness policy (`allow`, `warn`, `block`). +3. Continue to Meeting SDK join only for `allow`/approved `warn`. + +See [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md) and [../../general/use-cases/probe-sdk-preflight-readiness-gate.md](../../general/use-cases/probe-sdk-preflight-readiness-gate.md). + +## Client View vs Component View + +**CRITICAL DIFFERENCE**: These are two completely different APIs with different patterns! + +| Aspect | Client View | Component View | +|--------|-------------|----------------| +| **Object** | `ZoomMtg` (global singleton) | `ZoomMtgEmbedded.createClient()` (instance) | +| **API Style** | Callbacks | Promises | +| **UI** | Full-page takeover | Embeddable in any container | +| **Password param** | `passWord` (capital W) | `password` (lowercase) | +| **Events** | `inMeetingServiceListener()` | `on()`/`off()` | +| **Import (npm)** | `import { ZoomMtg } from '@zoom/meetingsdk'` | `import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'` | +| **CDN** | `zoom-meeting-{VERSION}.min.js` | `zoom-meeting-embedded-{VERSION}.min.js` | +| **Best For** | Quick integration, standard Zoom UI | Custom layouts, React/Vue apps | + +### When to Use Which + +**Use Client View when:** +- You want the familiar Zoom meeting interface +- Quick integration is priority over customization +- Full-page meeting experience is acceptable + +**Use Component View when:** +- You need to embed meetings in a specific area of your page +- Building React/Vue/Angular applications +- You want Promise-based async/await syntax +- Custom positioning and resizing is required + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/meetingsdk --save +``` + +### CDN + +```html + + + + + + + + + + + + +``` + +Replace `{VERSION}` with the [latest version](https://www.npmjs.com/package/@zoom/meetingsdk) (e.g., `3.11.0`). + +## Quick Start (Client View) + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Step 1: Check browser compatibility +console.log('System requirements:', ZoomMtg.checkSystemRequirements()); + +// Step 2: Preload WebAssembly for faster initialization +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Step 3: Load language files (MUST complete before init) +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + + // Step 4: Initialize SDK + ZoomMtg.init({ + leaveUrl: 'https://yoursite.com/meeting-ended', + disableCORP: !window.crossOriginIsolated, // Auto-detect SharedArrayBuffer + patchJsMedia: true, // Auto-apply media dependency fixes + leaveOnPageUnload: true, // Clean up when page unloads + externalLinkPage: './external.html', // Page for external links + success: () => { + + // Step 5: Join meeting (note: passWord with capital W!) + ZoomMtg.join({ + signature: signature, // From your auth endpoint + meetingNumber: '1234567890', + userName: 'User Name', + passWord: 'meeting-password', // Capital W! + success: (res) => { + console.log('Joined meeting:', res); + + // Post-join: Get meeting info + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => { + console.error('Join error:', err); + } + }); + }, + error: (err) => { + console.error('Init error:', err); + } + }); +}); +``` + +## Quick Start (Component View) + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Create client instance (do this ONCE, not on every render!) +const client = ZoomMtgEmbedded.createClient(); + +async function startMeeting() { + try { + // Initialize with container element + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + debug: true, // Enable debug logging + patchJsMedia: true, // Auto-apply media fixes + leaveOnPageUnload: true, // Clean up on page unload + }); + + // Join meeting (note: password lowercase!) + await client.join({ + signature: signature, // From your auth endpoint + sdkKey: SDK_KEY, + meetingNumber: '1234567890', + userName: 'User Name', + password: 'meeting-password', // Lowercase! + }); + + console.log('Joined successfully!'); + } catch (error) { + console.error('Failed to join:', error); + } +} +``` + +## Authentication Endpoint (Required) + +Both views require a JWT signature from a backend server. **Never expose your SDK Secret in frontend code!** + +```bash +# Clone Zoom's official auth endpoint +git clone https://github.com/zoom/meetingsdk-auth-endpoint-sample --depth 1 +cd meetingsdk-auth-endpoint-sample +cp .env.example .env +# Edit .env with your SDK Key and Secret +npm install && npm run start +``` + +### Signature Generation + +The signature encodes: +- `sdkKey` (or `clientId` for newer apps) +- `meetingNumber` +- `role` (0 = participant, 1 = host) +- `iat` (issued at timestamp) +- `exp` (expiration timestamp) +- `tokenExp` (token expiration) + +> **IMPORTANT (March 2026)**: Apps joining meetings outside their account will require an App Privilege Token (OBF) or ZAK token. See [Authorization Requirements](#authorization-requirements-2026-update). + +## Core Workflow + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Get Signature │───►│ init() │───►│ join() │ +│ (from backend)│ │ (SDK setup) │ │ (enter mtg) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + success/error success/error + callback callback + (or Promise resolve) (or Promise resolve) +``` + +## Client View API Reference + +### ZoomMtg.init() - Key Options + +```javascript +ZoomMtg.init({ + // Required + leaveUrl: string, // URL to redirect after leaving + + // Display Options + showMeetingHeader: boolean, // Show meeting number/topic (default: true) + disableInvite: boolean, // Hide invite button (default: false) + disableRecord: boolean, // Hide record button (default: false) + disableJoinAudio: boolean, // Hide join audio option (default: false) + disablePreview: boolean, // Skip A/V preview (default: false) + + // HD Video (requires SharedArrayBuffer) + enableHD: boolean, // Enable 720p (default: true for >=2.8.0) + enableFullHD: boolean, // Enable 1080p for webinars (default: false) + + // View Options + defaultView: 'gallery' | 'speaker' | 'multiSpeaker', + + // Feature Toggles + isSupportChat: boolean, // Enable chat (default: true) + isSupportCC: boolean, // Enable closed captions (default: true) + isSupportBreakout: boolean, // Enable breakout rooms (default: true) + isSupportPolling: boolean, // Enable polling (default: true) + isSupportQA: boolean, // Enable Q&A for webinars (default: true) + + // Cross-Origin + disableCORP: boolean, // For dev without COOP/COEP headers + + // Callbacks + success: Function, + error: Function, +}); +``` + +### ZoomMtg.join() - Key Options + +```javascript +ZoomMtg.join({ + // Required + signature: string, // JWT signature from backend + meetingNumber: string | number, + userName: string, + + // Authentication + passWord: string, // Meeting password (capital W!) + zak: string, // Host's ZAK token (required to start) + tk: string, // Registration token (if required) + obfToken: string, // App Privilege Token (for 2026 requirement) + + // Optional + userEmail: string, // Required for webinars + customerKey: string, // Custom identifier (max 36 chars) + + // Callbacks + success: Function, + error: Function, +}); +``` + +### Event Listeners (Client View) + +```javascript +// User events +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); + // data.reasonCode values: + // 0: OTHER + // 1: HOST_ENDED_MEETING + // 2: SELF_LEAVE_FROM_IN_MEETING + // 3: SELF_LEAVE_FROM_WAITING_ROOM + // 4: SELF_LEAVE_FROM_WAITING_FOR_HOST_START + // 5: MEETING_TRANSFER + // 6: KICK_OUT_FROM_MEETING + // 7: KICK_OUT_FROM_WAITING_ROOM + // 8: LEAVE_FROM_DISCLAIMER +}); + +ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('User updated:', data); +}); + +// Meeting status +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + console.log('Meeting status:', data.status); +}); + +// Waiting room +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); + +// Active speaker detection +ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + // [{userId: number, userName: string}] + console.log('Active speaker:', data); +}); + +// Network quality monitoring +ZoomMtg.inMeetingServiceListener('onNetworkQualityChange', (data) => { + // {level: 0-5, userId, type: 'uplink'} + // 0-1 = bad, 2 = normal, 3-5 = good + if (data.level <= 1) { + console.warn('Poor network quality'); + } +}); + +// Join performance metrics +ZoomMtg.inMeetingServiceListener('onJoinSpeed', (data) => { + console.log('Join speed metrics:', data); + // Useful for performance monitoring dashboards +}); + +// Chat +ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('Chat message:', data); +}); + +// Recording +ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('Recording status:', data); +}); + +// Screen sharing +ZoomMtg.inMeetingServiceListener('onShareContentChange', (data) => { + console.log('Share content changed:', data); +}); + +// Transcription (requires "save closed captions" enabled) +ZoomMtg.inMeetingServiceListener('onReceiveTranscriptionMsg', (data) => { + console.log('Transcription:', data); +}); + +// Breakout room status +ZoomMtg.inMeetingServiceListener('onRoomStatusChange', (data) => { + // status: 2=InProgress, 3=Closing, 4=Closed + console.log('Breakout room status:', data); +}); +``` + +### Common Methods (Client View) + +```javascript +// Get current user info +ZoomMtg.getCurrentUser({ + success: (res) => console.log(res.result.currentUser) +}); + +// Get all attendees +ZoomMtg.getAttendeeslist({}); + +// Audio/Video control +ZoomMtg.mute({ userId, mute: true }); +ZoomMtg.muteAll({ muteAll: true }); + +// Chat +ZoomMtg.sendChat({ message: 'Hello!', userId: 0 }); // 0 = everyone + +// Leave/End +ZoomMtg.leaveMeeting({}); +ZoomMtg.endMeeting({}); + +// Host controls +ZoomMtg.makeHost({ userId }); +ZoomMtg.makeCoHost({ oderId }); +ZoomMtg.expel({ userId }); // Remove participant +ZoomMtg.putOnHold({ oderId, bHold: true }); + +// Breakout rooms +ZoomMtg.createBreakoutRoom({ rooms: [...] }); +ZoomMtg.openBreakoutRooms({}); +ZoomMtg.closeBreakoutRooms({}); + +// Virtual background +ZoomMtg.setVirtualBackground({ imageUrl: '...' }); +``` + +## Component View API Reference + +### client.init() - Key Options + +```javascript +await client.init({ + // Required + zoomAppRoot: HTMLElement, // Container element + + // Display + language: string, // e.g., 'en-US' + debug: boolean, // Enable debug logging (default: false) + + // Media + patchJsMedia: boolean, // Auto-apply media fixes (default: false) + leaveOnPageUnload: boolean, // Clean up on page unload (default: false) + + // Video + enableHD: boolean, // Enable 720p + enableFullHD: boolean, // Enable 1080p + + // Customization + customize: { + video: { + isResizable: boolean, + viewSizes: { default: { width, height } } + }, + meetingInfo: ['topic', 'host', 'mn', 'pwd', 'telPwd', 'invite', 'participant', 'dc', 'enctype'], + toolbar: { + buttons: [ + { + text: 'Custom Button', + className: 'custom-btn', + onClick: () => { + console.log('Custom button clicked'); + } + } + ] + } + }, + + // For ZFG + webEndpoint: string, + assetPath: string, // Custom path for AV libraries (self-hosting) +}); +``` + +### client.join() - Key Options + +```javascript +await client.join({ + // Required + signature: string, + sdkKey: string, + meetingNumber: string | number, + userName: string, + + // Authentication + password: string, // Lowercase! (different from Client View) + zak: string, // Host's ZAK token + tk: string, // Registration token + + // Optional + userEmail: string, +}); +``` + +### Event Listeners (Component View) + +```javascript +// Connection state +client.on('connection-change', (payload) => { + // payload.state: 'Connecting', 'Connected', 'Reconnecting', 'Closed' + console.log('Connection:', payload.state); +}); + +// User events +client.on('user-added', (payload) => { + console.log('Users added:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('Users removed:', payload); +}); + +client.on('user-updated', (payload) => { + console.log('Users updated:', payload); +}); + +// Active speaker +client.on('active-speaker', (payload) => { + console.log('Active speaker:', payload); +}); + +// Video state +client.on('video-active-change', (payload) => { + console.log('Video active:', payload); +}); + +// Unsubscribe +client.off('connection-change', handler); +``` + +### Common Methods (Component View) + +```javascript +// Get current user +const currentUser = client.getCurrentUser(); + +// Get all participants +const participants = client.getParticipantsList(); + +// Audio control +await client.mute(true); +await client.muteAudio(userId, true); + +// Video control +await client.muteVideo(userId, true); + +// Leave +client.leaveMeeting(); + +// End (host only) +client.endMeeting(); +``` + +## SharedArrayBuffer (CRITICAL for HD) + +SharedArrayBuffer enables advanced features: +- 720p/1080p video +- Gallery view +- Virtual backgrounds +- Background noise suppression + +### Enable with HTTP Headers + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +### Verify in Browser + +```javascript +if (typeof SharedArrayBuffer === 'function') { + console.log('SharedArrayBuffer enabled!'); +} else { + console.warn('HD features will be limited'); +} + +// Or check cross-origin isolation +console.log('Cross-origin isolated:', window.crossOriginIsolated); +``` + +### Platform-Specific Setup + +See [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) for: +- Vercel, Netlify, AWS CloudFront configuration +- nginx/Apache configuration +- Service worker fallback for GitHub Pages + +### Development Setup (Two-Server Pattern) + +The official samples use a **two-server pattern** for development because COOP/COEP headers can break navigation: + +```javascript +// Server 1: Main app (port 9999) - NO isolation headers +// Serves index.html, navigation works normally + +// Server 2: Meeting page (port 9998) - WITH isolation headers +// Serves meeting.html with SharedArrayBuffer support + +// Main server proxies to meeting server +proxy: [{ + path: '/meeting.html', + target: 'http://YOUR_MEETING_SERVER_HOST:9998/' +}] +``` + +**Vite config with headers:** +```typescript +// vite.config.ts +export default defineConfig({ + server: { + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + } + } +}); +``` + +## Common Issues & Solutions + +| Issue | Solution | +|-------|----------| +| **Join fails with signature error** | Verify signature generation, check sdkKey format | +| **"passWord" typo** | Client View uses `passWord` (capital W), Component View uses `password` | +| **No HD video** | Enable SharedArrayBuffer headers, check browser support | +| **Callbacks not firing** | Ensure `inMeetingServiceListener` called after init success | +| **Virtual background not working** | Requires SharedArrayBuffer + Chrome/Edge | +| **Screen share fails on Safari** | Safari 17+ with macOS 14+ required for client view | + +**Complete troubleshooting**: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Browser Support Matrix + +| Feature | Chrome | Firefox | Safari | Edge | iOS | Android | +|---------|--------|---------|--------|------|-----|---------| +| 720p (receive) | Yes | Yes | Yes | Yes | Yes | Yes | +| 720p (send) | Yes* | Yes* | Yes* | Yes* | Yes* | Yes* | +| Virtual background | Yes | Yes | No | Yes | No | No | +| Screen share (send) | Yes | Yes | Safari 17+ | Yes | No | No | +| Gallery view | Yes | Yes | Yes** | Yes | Yes | Yes | + +*Requires SharedArrayBuffer +**Safari 17+ with macOS Sonoma + +See [concepts/browser-support.md](concepts/browser-support.md) for complete matrix. + +## Authorization Requirements (2026 Update) + +> **IMPORTANT**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +### Options + +1. **App Privilege Token (OBF)** - Recommended for bots + ```javascript + ZoomMtg.join({ + ... + obfToken: 'your-app-privilege-token' + }); + ``` + +2. **ZAK Token** - For host operations + ```javascript + ZoomMtg.join({ + ... + zak: 'host-zak-token' + }); + ``` + +## Zoom for Government (ZFG) + +### Option 1: ZFG-specific NPM Package + +```json +{ + "dependencies": { + "@zoom/meetingsdk": "3.11.2-zfg" + } +} +``` + +### Option 2: Configure ZFG Endpoints + +**Client View:** +```javascript +ZoomMtg.setZoomJSLib('https://source.zoomgov.com/{VERSION}/lib', '/av'); +ZoomMtg.init({ + webEndpoint: 'www.zoomgov.com', + ... +}); +``` + +**Component View:** +```javascript +await client.init({ + webEndpoint: 'www.zoomgov.com', + assetPath: 'https://source.zoomgov.com/{VERSION}/lib/av', + ... +}); +``` + +## China CDN + +```javascript +// Set before preLoadWasm() +ZoomMtg.setZoomJSLib('https://jssdk.zoomus.cn/{VERSION}/lib', '/av'); +``` + +## React Integration + +### Official Pattern (from zoom/meetingsdk-react-sample) + +The official React sample uses **imperative initialization** rather than React hooks: + +```tsx +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Preload at module level (outside component) +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +function App() { + const authEndpoint = import.meta.env.VITE_AUTH_ENDPOINT; + const meetingNumber = ''; + const passWord = ''; + const role = 0; + const userName = 'React User'; + + const getSignature = async () => { + const response = await fetch(authEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + meetingNumber, + role, + }), + }); + const data = await response.json(); + startMeeting(data.signature); + }; + + const startMeeting = (signature: string) => { + document.getElementById('zmmtg-root')!.style.display = 'block'; + + ZoomMtg.init({ + leaveUrl: window.location.origin, + patchJsMedia: true, + leaveOnPageUnload: true, + success: () => { + ZoomMtg.join({ + signature, + meetingNumber, + userName, + passWord, + success: (res) => console.log('Joined:', res), + error: (err) => console.error('Join error:', err), + }); + }, + error: (err) => console.error('Init error:', err), + }); + }; + + return ( + + ); +} +``` + +### React Gotchas (from official samples) + +| Issue | Problem | Solution | +|-------|---------|----------| +| **Client Recreation** | `createClient()` in component body runs every render | Use `useRef` to persist client | +| **No useEffect** | Official sample doesn't use React lifecycle hooks | SDK's `leaveOnPageUnload` handles cleanup | +| **Direct DOM** | Sample uses `getElementById` | Use `useRef` in production | +| **No Error State** | Silent failures | Add `useState` for error handling | +| **Module-Scope Side Effects** | `preLoadWasm()` at top level | May cause issues with SSR | + +### Production-Ready React Pattern + +```tsx +import { useEffect, useRef, useState, useCallback } from 'react'; +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +type ZoomClient = ReturnType; + +function ZoomMeeting({ meetingNumber, password, userName }: Props) { + const clientRef = useRef(null); + const containerRef = useRef(null); + const [isJoining, setIsJoining] = useState(false); + const [error, setError] = useState(null); + + // Create client once + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); + + const joinMeeting = useCallback(async () => { + if (!clientRef.current || !containerRef.current) return; + + setIsJoining(true); + setError(null); + + try { + // Get signature from your backend + const response = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + const { signature, sdkKey } = await response.json(); + + await clientRef.current.init({ + zoomAppRoot: containerRef.current, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await clientRef.current.join({ + signature, + sdkKey, + meetingNumber, + password, + userName, + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to join'); + } finally { + setIsJoining(false); + } + }, [meetingNumber, password, userName]); + + return ( +
+
+ + {error &&
{error}
} +
+ ); +} +``` + +### Environment Variables (Vite) + +```bash +# .env.local +VITE_AUTH_ENDPOINT=http://YOUR_AUTH_SERVER_HOST:4000 +VITE_SDK_KEY=your_sdk_key +``` + +```tsx +const authEndpoint = import.meta.env.VITE_AUTH_ENDPOINT; +const sdkKey = import.meta.env.VITE_SDK_KEY; +``` + +## Detailed References + +### Core Documentation +- **[SKILL.md](SKILL.md)** - Complete navigation guide +- **[client-view/SKILL.md](client-view/SKILL.md)** - Full Client View reference +- **[component-view/SKILL.md](component-view/SKILL.md)** - Full Component View reference + +### Concepts +- **[concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md)** - HD video requirements +- **[concepts/browser-support.md](concepts/browser-support.md)** - Feature matrix by browser + +### Troubleshooting +- **[troubleshooting/error-codes.md](troubleshooting/error-codes.md)** - All SDK error codes +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Quick diagnostics + +### Examples +- **[client-view/SKILL.md](client-view/SKILL.md)** - Complete Client View guide +- **[component-view/SKILL.md](component-view/SKILL.md)** - Component View React integration + +## Helper Utilities + +### Extract Meeting Number from Invite Link + +```javascript +// Users can paste full Zoom invite links +document.getElementById('meeting_number').addEventListener('input', (e) => { + // Extract meeting number (9-11 digits) + let meetingNumber = e.target.value.replace(/([^0-9])+/i, ''); + if (meetingNumber.match(/([0-9]{9,11})/)) { + meetingNumber = meetingNumber.match(/([0-9]{9,11})/)[1]; + } + + // Auto-extract password from invite link + const pwdMatch = e.target.value.match(/pwd=([\d,\w]+)/); + if (pwdMatch) { + document.getElementById('password').value = pwdMatch[1]; + } +}); +``` + +### Dynamic Language Switching + +```javascript +// Change language at runtime +document.getElementById('language').addEventListener('change', (e) => { + const lang = e.target.value; + ZoomMtg.i18n.load(lang); + ZoomMtg.i18n.reload(lang); + ZoomMtg.reRender({ lang }); +}); +``` + +### Check System Requirements + +```javascript +// Check browser compatibility before initializing +const requirements = ZoomMtg.checkSystemRequirements(); +console.log('Browser info:', JSON.stringify(requirements)); + +if (!requirements.browserInfo.isChrome && !requirements.browserInfo.isFirefox) { + alert('For best experience, use Chrome or Firefox'); +} +``` + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | Official samples (Client View & Component View) | +| [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | React integration with TypeScript + Vite | +| [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | SDK source with helper.html | +| [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | Signature generation backend | + +## Official Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Client View API Reference**: https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- **Component View API Reference**: https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html +- **Developer forum**: https://devforum.zoom.us/ + +--- + +**Documentation Version**: Based on Zoom Web Meeting SDK v3.11+ + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from meeting-sdk/web/SKILL.md + +# Zoom Meeting SDK (Web) - Documentation Index + +Quick navigation guide for all Web SDK documentation. + +## Start Here + +| Document | Description | +|----------|-------------| +| **[SKILL.md](SKILL.md)** | Main entry point - Quick starts for both Client View and Component View | + +## By View Type + +### Client View (Full-Page) +| Document | Description | +|----------|-------------| +| **[client-view/SKILL.md](client-view/SKILL.md)** | Complete Client View reference | + +### Component View (Embeddable) +| Document | Description | +|----------|-------------| +| **[component-view/SKILL.md](component-view/SKILL.md)** | Complete Component View reference | + +## Concepts + +| Document | Description | +|----------|-------------| +| **[concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md)** | HD video requirements, COOP/COEP headers | +| **[concepts/browser-support.md](concepts/browser-support.md)** | Feature matrix by browser | + +## Examples + +| Document | Description | +|----------|-------------| +| [examples/client-view-basic.md](examples/client-view-basic.md) | Basic Client View integration | +| [examples/component-view-react.md](examples/component-view-react.md) | React integration with Component View | + +## Troubleshooting + +| Document | Description | +|----------|-------------| +| **[troubleshooting/error-codes.md](troubleshooting/error-codes.md)** | All SDK error codes (3000-10000 range) | +| **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** | Quick diagnostics and fixes | + +## By Topic + +### Authentication +- [SKILL.md#authentication-endpoint](SKILL.md#authentication-endpoint-required) - Signature generation +- [SKILL.md#authorization-requirements-2026-update](SKILL.md#authorization-requirements-2026-update) - OBF tokens + +### HD Video & Performance +- [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) - Enable 720p/1080p + +### Events & Callbacks +- [SKILL.md#event-listeners-client-view](SKILL.md#event-listeners-client-view) - Client View events +- [SKILL.md#event-listeners-component-view](SKILL.md#event-listeners-component-view) - Component View events + +### Government (ZFG) +- [SKILL.md#zoom-for-government-zfg](SKILL.md#zoom-for-government-zfg) - ZFG configuration + +### China CDN +- [SKILL.md#china-cdn](SKILL.md#china-cdn) - China-specific CDN + +## Quick Reference + +### Client View vs Component View + +| Aspect | Client View | Component View | +|--------|-------------|----------------| +| **Object** | `ZoomMtg` | `ZoomMtgEmbedded.createClient()` | +| **API Style** | Callbacks | Promises | +| **Password param** | `passWord` (capital W) | `password` (lowercase) | +| **Events** | `inMeetingServiceListener()` | `on()`/`off()` | + +### Key Gotchas + +1. **Password spelling differs between views!** + - Client View: `passWord` (capital W) + - Component View: `password` (lowercase) + +2. **SharedArrayBuffer required for HD features** + - 720p/1080p video + - Gallery view (25 videos) + - Virtual backgrounds + +3. **March 2026 Authorization Change** + - Apps joining external meetings need OBF or ZAK tokens + +## External Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Client View API**: https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- **Component View API**: https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html +- **GitHub samples**: https://github.com/zoom/meetingsdk-web-sample + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/RUNBOOK.md new file mode 100644 index 00000000..a10362fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Web Client View 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Web Client View (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/client-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/client-view/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/SKILL.md new file mode 100644 index 00000000..894296a9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/SKILL.md @@ -0,0 +1,620 @@ +--- +name: zoom-meeting-sdk-web-client-view +description: | + Zoom Meeting SDK Web - Client View. Full-page Zoom meeting experience with the familiar Zoom interface. + Uses ZoomMtg global singleton with callback-based API. Ideal for quick integration with minimal + customization. Provides the same UI as Zoom Web Client. +user-invocable: false +triggers: + - "meeting sdk client view" + - "zoommtg" + - "full page zoom ui" + - "password" + - "preparewebsdk" +--- + +# Zoom Meeting SDK Web - Client View + +Full-page Zoom meeting experience embedded in your web application. Client View provides the familiar Zoom interface with minimal customization needed. + +## Overview + +Client View uses the `ZoomMtg` global singleton to render a full-page meeting experience identical to the Zoom Web Client. + +| Aspect | Details | +|--------|---------| +| **API Object** | `ZoomMtg` (global singleton) | +| **API Style** | Callback-based | +| **UI** | Full-page takeover | +| **Password param** | `passWord` (capital W) | +| **Events** | `inMeetingServiceListener()` | +| **Best For** | Quick integration, standard Zoom UI | + +## Installation + +### NPM + +```bash +npm install @zoom/meetingsdk --save +``` + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; +``` + +### CDN + +```html + + + + + + +``` + +## Complete Initialization Flow + +```javascript +// Step 1: Check browser compatibility +console.log('Requirements:', ZoomMtg.checkSystemRequirements()); + +// Step 2: Set CDN path (optional - for China or custom hosting) +// ZoomMtg.setZoomJSLib('https://source.zoom.us/{VERSION}/lib', '/av'); + +// Step 3: Preload WebAssembly modules +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Step 4: Load language resources +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + + // Step 5: Initialize SDK + ZoomMtg.init({ + leaveUrl: '/meeting-ended', + patchJsMedia: true, + disableCORP: !window.crossOriginIsolated, + success: () => { + console.log('SDK initialized'); + + // Step 6: Join meeting + const joinPayload = { + signature: signature, + meetingNumber: meetingNumber, + userName: userName, + passWord: passWord, + success: (res) => { + console.log('Joined meeting'); + + // Step 7: Post-join operations + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => console.error('Join failed:', err) + }; + + // IMPORTANT: only include optional fields when they have real values + // Passing undefined for some optional fields can cause runtime join errors + if (userEmail) joinPayload.userEmail = userEmail; + if (tk) joinPayload.tk = tk; + if (zak) joinPayload.zak = zak; + + ZoomMtg.join(joinPayload); + }, + error: (err) => console.error('Init failed:', err) + }); +}); +``` + +## ZoomMtg.init() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `leaveUrl` | `string` | URL to redirect after leaving meeting | + +### UI Customization + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `showMeetingHeader` | `boolean` | `true` | Show meeting number and topic | +| `disableInvite` | `boolean` | `false` | Hide invite button | +| `disableCallOut` | `boolean` | `false` | Hide call out option | +| `disableRecord` | `boolean` | `false` | Hide record button | +| `disableJoinAudio` | `boolean` | `false` | Hide join audio option | +| `disablePreview` | `boolean` | `false` | Skip audio/video preview | +| `audioPanelAlwaysOpen` | `boolean` | `false` | Keep audio panel open | +| `showPureSharingContent` | `boolean` | `false` | Prevent overlays on shared content | +| `videoHeader` | `boolean` | `true` | Show video tile header | +| `isLockBottom` | `boolean` | `true` | Show/hide footer | +| `videoDrag` | `boolean` | `true` | Enable drag video tiles | +| `sharingMode` | `string` | `'both'` | `'both'` or `'fit'` | +| `screenShare` | `boolean` | `true` | Enable browser URL sharing | +| `hideShareAudioOption` | `boolean` | `false` | Hide "Share tab audio" checkbox | +| `disablePictureInPicture` | `boolean` | `false` | Disable PiP mode | +| `disableZoomLogo` | `boolean` | `false` | Remove Zoom logo (deprecated) | +| `defaultView` | `string` | `'speaker'` | `'gallery'`, `'speaker'`, `'multiSpeaker'` | + +### Feature Toggles + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `isSupportAV` | `boolean` | `true` | Enable audio/video | +| `isSupportChat` | `boolean` | `true` | Enable in-meeting chat | +| `isSupportQA` | `boolean` | `true` | Enable webinar Q&A | +| `isSupportCC` | `boolean` | `true` | Enable closed captions | +| `isSupportPolling` | `boolean` | `true` | Enable polling | +| `isSupportBreakout` | `boolean` | `true` | Enable breakout rooms | +| `isSupportNonverbal` | `boolean` | `true` | Enable nonverbal feedback | +| `isSupportSimulive` | `boolean` | `false` | Enable Simulive | +| `disableVoIP` | `boolean` | `false` | Disable VoIP | +| `disableReport` | `boolean` | `false` | Disable report feature | + +### Video Quality + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `enableHD` | `boolean` | `true` (≥2.8.0) | Enable 720p video | +| `enableFullHD` | `boolean` | `false` | Enable 1080p for webinar attendees | + +### Advanced + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `debug` | `boolean` | `false` | Enable debug logging | +| `patchJsMedia` | `boolean` | `false` | Auto-apply media fixes | +| `disableCORP` | `boolean` | `false` | Disable web isolation | +| `helper` | `string` | `''` | Path to helper.html | +| `externalLinkPage` | `string` | - | Page for external links | +| `webEndpoint` | `string` | - | For ZFG environments | +| `leaveOnPageUnload` | `boolean` | `false` | Auto cleanup on page close | +| `isShowJoiningErrorDialog` | `boolean` | `true` | Show error dialog on join failure | +| `meetingInfo` | `Array` | `[...]` | Meeting info to display | +| `inviteUrlFormat` | `string` | `''` | Custom invite URL format | +| `loginWindow` | `object` | `{width: 400, height: 380}` | Login popup size | + +### Callbacks + +| Parameter | Type | Description | +|-----------|------|-------------| +| `success` | `Function` | Called on successful init | +| `error` | `Function` | Called on init failure | + +## ZoomMtg.join() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `signature` | `string` | SDK JWT from backend (v5.0+: must include appKey prefix) | +| `meetingNumber` | `string \| number` | Meeting or webinar number | +| `userName` | `string` | Display name | +| `passWord` | `string` | Meeting password (capital W!) | + +### Authentication + +| Parameter | Type | When Required | Description | +|-----------|------|---------------|-------------| +| `zak` | `string` | Starting as host | Host's Zoom Access Key | +| `tk` | `string` | Registration required | Registrant token | +| `userEmail` | `string` | Webinars | User email | +| `obfToken` | `string` | March 2026+ | App Privilege Token | + +### Optional + +| Parameter | Type | Description | +|-----------|------|-------------| +| `customerKey` | `string` | Custom ID (max 36 chars) | +| `recordingToken` | `string` | Local recording permission | + +### Callbacks + +| Parameter | Type | Description | +|-----------|------|-------------| +| `success` | `Function` | Called on successful join | +| `error` | `Function` | Called on join failure | + +## Event Listeners + +### User Events + +```javascript +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); + // { userId, userName, ... } +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); + // Reason codes: + // 0: OTHER + // 1: HOST_ENDED_MEETING + // 2: SELF_LEAVE_FROM_IN_MEETING + // 3: SELF_LEAVE_FROM_WAITING_ROOM + // 4: SELF_LEAVE_FROM_WAITING_FOR_HOST_START + // 5: MEETING_TRANSFER + // 6: KICK_OUT_FROM_MEETING + // 7: KICK_OUT_FROM_WAITING_ROOM + // 8: LEAVE_FROM_DISCLAIMER +}); + +ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('User updated:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); +``` + +### Meeting Status + +```javascript +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + console.log('Status:', data.status); +}); +``` + +### Audio/Video Events + +```javascript +ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + // [{userId, userName}] + console.log('Active speaker:', data); +}); + +ZoomMtg.inMeetingServiceListener('onNetworkQualityChange', (data) => { + // {level: 0-5, userId, type: 'uplink'} + // 0-1=bad, 2=normal, 3-5=good + console.log('Network quality:', data); +}); + +ZoomMtg.inMeetingServiceListener('onAudioQos', (data) => { + console.log('Audio QoS:', data); +}); + +ZoomMtg.inMeetingServiceListener('onVideoQos', (data) => { + console.log('Video QoS:', data); +}); +``` + +### Chat & Communication + +```javascript +ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('Chat message:', data); +}); + +ZoomMtg.inMeetingServiceListener('onReceiveTranscriptionMsg', (data) => { + console.log('Transcription:', data); +}); + +ZoomMtg.inMeetingServiceListener('onReceiveTranslateMsg', (data) => { + console.log('Translation:', data); +}); +``` + +### Recording & Sharing + +```javascript +ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('Recording status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onShareContentChange', (data) => { + console.log('Share content:', data); +}); + +ZoomMtg.inMeetingServiceListener('receiveSharingChannelReady', (data) => { + console.log('Sharing channel ready:', data); +}); +``` + +### Breakout Rooms + +```javascript +ZoomMtg.inMeetingServiceListener('onRoomStatusChange', (data) => { + // status: 2=InProgress, 3=Closing, 4=Closed + console.log('Breakout room status:', data); +}); +``` + +### Other Events + +```javascript +ZoomMtg.inMeetingServiceListener('onJoinSpeed', (data) => { + console.log('Join metrics:', data); +}); + +ZoomMtg.inMeetingServiceListener('onVbStatusChange', (data) => { + console.log('Virtual background status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onFocusModeStatusChange', (data) => { + console.log('Focus mode:', data); +}); + +ZoomMtg.inMeetingServiceListener('onPictureInPicture', (data) => { + console.log('PiP status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onClaimStatus', (data) => { + console.log('Host claim status:', data); +}); +``` + +## Common Methods + +### Meeting Info + +```javascript +// Get current user +ZoomMtg.getCurrentUser({ + success: (res) => console.log(res.result.currentUser) +}); + +// Get all attendees +ZoomMtg.getAttendeeslist({}); + +// Get meeting info +ZoomMtg.getCurrentMeetingInfo({ + success: (res) => console.log(res) +}); + +// Get SDK version +ZoomMtg.getWebSDKVersion({ + success: (version) => console.log(version) +}); +``` + +### Audio/Video Control + +```javascript +// Mute/unmute specific user +ZoomMtg.mute({ userId, mute: true }); + +// Mute/unmute all +ZoomMtg.muteAll({ muteAll: true }); + +// Stop incoming audio +ZoomMtg.stopIncomingAudio({ stop: true }); + +// Mirror video +ZoomMtg.mirrorVideo({ mirror: true }); +``` + +### Chat + +```javascript +// Send chat message +ZoomMtg.sendChat({ + message: 'Hello!', + userId: 0 // 0 = everyone +}); +``` + +### Meeting Control + +```javascript +// Leave meeting +ZoomMtg.leaveMeeting({}); + +// End meeting (host only) +ZoomMtg.endMeeting({}); + +// Lock meeting +ZoomMtg.lockMeeting({ lock: true }); +``` + +### Host Controls + +```javascript +// Make host +ZoomMtg.makeHost({ userId }); + +// Make co-host +ZoomMtg.makeCoHost({ userId }); + +// Remove co-host +ZoomMtg.withdrawCoHost({ userId }); + +// Remove participant +ZoomMtg.expel({ userId }); + +// Put on hold +ZoomMtg.putOnHold({ userId, bHold: true }); + +// Claim host with host key +ZoomMtg.claimHostWithHostKey({ hostKey: '123456' }); + +// Reclaim host +ZoomMtg.reclaimHost({}); + +// Admit all from waiting room +ZoomMtg.admitAll({}); +``` + +### Raise Hand + +```javascript +// Raise hand +ZoomMtg.raiseHand({ userId }); + +// Lower hand +ZoomMtg.lowerHand({ oderId }); + +// Lower all hands +ZoomMtg.lowerAllHands({}); +``` + +### Spotlight & Pin + +```javascript +// Spotlight video +ZoomMtg.operateSpotlight({ oderId, action: 'add' }); // or 'remove' + +// Pin video +ZoomMtg.operatePin({ oderId, action: 'add' }); // or 'remove' + +// Allow multi-pin +ZoomMtg.allowMultiPin({ allow: true }); +``` + +### Screen Share + +```javascript +// Start screen share +ZoomMtg.startScreenShare({}); + +// Share specific source (Electron) +ZoomMtg.shareSource({ source }); +``` + +### Recording + +```javascript +// Start/stop recording +ZoomMtg.record({ record: true }); // or false + +// Show/hide record button +ZoomMtg.showRecordFunction({ show: true }); +``` + +### Breakout Rooms + +```javascript +// Create breakout room +ZoomMtg.createBreakoutRoom({ + rooms: [{ name: 'Room 1' }, { name: 'Room 2' }] +}); + +// Open breakout rooms +ZoomMtg.openBreakoutRooms({}); + +// Close breakout rooms +ZoomMtg.closeBreakoutRooms({}); + +// Join breakout room +ZoomMtg.joinBreakoutRoom({ roomId }); + +// Leave breakout room +ZoomMtg.leaveBreakoutRoom({}); + +// Move user to breakout room +ZoomMtg.moveUserToBreakoutRoom({ oderId, roomId }); + +// Get breakout room status +ZoomMtg.getBreakoutRoomStatus({ + success: (res) => console.log(res) +}); +``` + +### Virtual Background + +```javascript +// Check support +ZoomMtg.isSupportVirtualBackground({ + success: (data) => console.log(data.result.isSupport) +}); + +// Set virtual background +ZoomMtg.setVirtualBackground({ imageUrl: '...' }); + +// Get VB status +ZoomMtg.getVirtualBackgroundStatus({ + success: (data) => console.log(data) +}); + +// Lock virtual background (host) +ZoomMtg.lockVirtualBackground({ lock: true }); +``` + +### UI Control + +```javascript +// Show/hide meeting header +ZoomMtg.showMeetingHeader({ show: true }); + +// Show/hide invite button +ZoomMtg.showInviteFunction({ show: true }); + +// Show/hide join audio button +ZoomMtg.showJoinAudioFunction({ show: true }); + +// Show/hide callout button +ZoomMtg.showCalloutFunction({ show: true }); + +// Re-render with new options +ZoomMtg.reRender({ lang: 'de-DE' }); +``` + +### Language + +```javascript +// Load language +ZoomMtg.i18n.load('de-DE'); + +// Reload language +ZoomMtg.i18n.reload('de-DE'); + +// Get current language +ZoomMtg.i18n.getCurrentLang(); + +// Get all translations +ZoomMtg.i18n.getAll(); +``` + +## Rate Limits + +| Method | Limit | +|--------|-------| +| `join()` | 10 seconds | +| `callOut()` | 10 seconds | +| `mute()` | 1 second | +| `muteAll()` | 5 seconds | + +## DOM Elements + +The SDK automatically adds these elements: +- `#zmmtg-root` - Main meeting container +- `#aria-notify-area` - Accessibility announcements + +Do NOT manually create or remove these. + +### SPA (React/Next) Overlay Gotcha + +If you "join" but see a blank/black area or your app shell instead of the meeting UI, the Zoom UI can be rendering **behind** your app layout. Ensure `#zmmtg-root` occupies the viewport and is above other fixed elements: + +```css +#zmmtg-root { + position: fixed !important; + inset: 0 !important; + z-index: 9999 !important; +} +``` + +### Join Payload Sanitization Gotcha + +If `ZoomMtg.join()` succeeds partially but the screen turns black and console shows errors like `Cannot read properties of undefined (reading 'toString')`, sanitize optional fields before calling join. + +Do not pass optional keys with `undefined` values (`userEmail`, `tk`, `zak`, etc.). Build a payload and only attach those keys when they are non-empty strings. + +Also prefer `defaultView: 'speaker'` during `ZoomMtg.init()` unless you have SharedArrayBuffer/gallery-view prerequisites fully configured. + +## Resources + +- [Main Web SDK Skill](../SKILL.md) +- [Reference Index](references/README.md) +- [Error Codes](../troubleshooting/error-codes.md) +- [Common Issues](../troubleshooting/common-issues.md) +- [SharedArrayBuffer Setup](../concepts/sharedarraybuffer.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/index.html) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/references/README.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/references/README.md new file mode 100644 index 00000000..8840e6b7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/client-view/references/README.md @@ -0,0 +1,9 @@ +# Client View Reference Index + +Use this index when you need the stable entrypoints around Client View behavior before deeper per-method pages are expanded. + +- [Client View Skill](../SKILL.md) +- [SharedArrayBuffer Setup](../../concepts/sharedarraybuffer.md) +- [Error Codes](../../troubleshooting/error-codes.md) +- [Common Issues](../../troubleshooting/common-issues.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/index.html) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/RUNBOOK.md new file mode 100644 index 00000000..d98726cb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Web Component View 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Web Component View (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/component-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/component-view/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/SKILL.md new file mode 100644 index 00000000..46062570 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/SKILL.md @@ -0,0 +1,620 @@ +--- +name: zoom-meeting-sdk-web-component-view +description: | + Zoom Meeting SDK Web - Component View. Embeddable Zoom meeting components with Promise-based API + for flexible integration. Ideal for React/Vue/Angular apps and custom layouts. Uses ZoomMtgEmbedded + with async/await patterns and embeddable UI containers. +user-invocable: false +triggers: + - "meeting sdk component view" + - "zoommtgembedded" + - "zoomapproot" + - "embeddable meeting ui" + - "component view embedded zoom" + - "custom meeting ui" + - "custom zoom meeting ui" + - "custom meeting video ui" + - "custom video ui for meeting" +--- + +# Zoom Meeting SDK Web - Component View + +Embeddable Zoom meeting components for flexible integration into any web application. Component View provides Promise-based APIs and customizable UI. + +This is the correct web skill for a **custom UI around a real Zoom meeting**. +Do not route to Video SDK unless the user is building a non-meeting custom session product. + +## Overview + +Component View uses `ZoomMtgEmbedded.createClient()` to create embeddable meeting components within a specific container element. + +| Aspect | Details | +|--------|---------| +| **API Object** | `ZoomMtgEmbedded.createClient()` (instance) | +| **API Style** | Promise-based (async/await) | +| **UI** | Embeddable in any container | +| **Password param** | `password` (lowercase) | +| **Events** | `on()`/`off()` | +| **Best For** | Custom layouts, React/Vue/Angular apps | + +## Installation + +### NPM + +```bash +npm install @zoom/meetingsdk --save +``` + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; +``` + +### CDN + +```html + + + + + + +``` + +## Complete Initialization Flow + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Step 1: Create client instance (do once, not on every render!) +const client = ZoomMtgEmbedded.createClient(); + +async function joinMeeting() { + try { + // Step 2: Get container element + const meetingSDKElement = document.getElementById('meetingSDKElement'); + + // Step 3: Initialize client + await client.init({ + zoomAppRoot: meetingSDKElement, + language: 'en-US', + debug: true, + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + // Step 4: Join meeting + await client.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + userName: userName, + password: password, // lowercase! + userEmail: userEmail, + }); + + console.log('Joined successfully!'); + } catch (error) { + console.error('Failed to join:', error); + } +} +``` + +## client.init() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `zoomAppRoot` | `HTMLElement` | Container element for meeting UI | + +### Display + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `language` | `string` | `'en-US'` | UI language | +| `debug` | `boolean` | `false` | Enable debug logging | + +### Media + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `patchJsMedia` | `boolean` | `false` | Auto-apply media fixes | +| `leaveOnPageUnload` | `boolean` | `false` | Cleanup on page unload | +| `enableHD` | `boolean` | `true` | Enable 720p video | +| `enableFullHD` | `boolean` | `false` | Enable 1080p video | + +### Customization + +| Parameter | Type | Description | +|-----------|------|-------------| +| `customize` | `object` | UI customization options | +| `webEndpoint` | `string` | For ZFG: `'www.zoomgov.com'` | +| `assetPath` | `string` | Custom path for AV libraries | + +### Customize Object + +```javascript +await client.init({ + zoomAppRoot: element, + customize: { + // Meeting info displayed + meetingInfo: [ + 'topic', + 'host', + 'mn', + 'pwd', + 'telPwd', + 'invite', + 'participant', + 'dc', + 'enctype' + ], + + // Video customization + video: { + isResizable: true, + viewSizes: { + default: { + width: 1000, + height: 600 + }, + ribbon: { + width: 300, + height: 700 + } + }, + popper: { + disableDraggable: false + } + }, + + // Custom toolbar buttons + toolbar: { + buttons: [ + { + text: 'Custom Button', + className: 'custom-btn', + onClick: () => { + console.log('Custom button clicked'); + } + } + ] + }, + + // Active speaker indicator + activeSpaker: { + strokeColor: '#00FF00' + } + } +}); +``` + +## client.join() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `signature` | `string` | SDK JWT from backend | +| `sdkKey` | `string` | SDK Key / Client ID | +| `meetingNumber` | `string \| number` | Meeting number | +| `userName` | `string` | Display name | + +### Authentication + +| Parameter | Type | When Required | Description | +|-----------|------|---------------|-------------| +| `password` | `string` | If set | Meeting password (lowercase!) | +| `zak` | `string` | Starting as host | Host's ZAK token | +| `tk` | `string` | Registration | Registrant token | +| `userEmail` | `string` | Webinars | User email | + +## Event Listeners + +### Syntax + +```javascript +// Subscribe +client.on('event-name', callback); + +// Unsubscribe +client.off('event-name', callback); +``` + +### Connection Events + +```javascript +client.on('connection-change', (payload) => { + // payload.state: 'Connecting', 'Connected', 'Reconnecting', 'Closed' + console.log('Connection state:', payload.state); + + if (payload.state === 'Closed') { + console.log('Reason:', payload.reason); + } +}); +``` + +### User Events + +```javascript +client.on('user-added', (payload) => { + // Array of users who joined + console.log('Users added:', payload); + payload.forEach(user => { + console.log('User ID:', user.oderId); + console.log('Name:', user.displayName); + }); +}); + +client.on('user-removed', (payload) => { + // Array of users who left + console.log('Users removed:', payload); +}); + +client.on('user-updated', (payload) => { + // Array of users whose properties changed + console.log('Users updated:', payload); +}); +``` + +### Audio Events + +```javascript +client.on('active-speaker', (payload) => { + // Current active speaker + console.log('Active speaker:', payload); +}); + +client.on('audio-statistic-data-change', (payload) => { + console.log('Audio stats:', payload); +}); +``` + +### Video Events + +```javascript +client.on('video-active-change', (payload) => { + // Video state changed + console.log('Video active:', payload); +}); + +client.on('video-statistic-data-change', (payload) => { + console.log('Video stats:', payload); +}); +``` + +### Share Events + +```javascript +client.on('active-share-change', (payload) => { + console.log('Share status:', payload); +}); + +client.on('share-statistic-data-change', (payload) => { + console.log('Share stats:', payload); +}); +``` + +### Chat Events + +```javascript +client.on('chat-on-message', (payload) => { + console.log('Chat message:', payload); +}); +``` + +### Recording Events + +```javascript +client.on('recording-change', (payload) => { + console.log('Recording status:', payload); +}); +``` + +### Media Device Events + +```javascript +client.on('media-sdk-change', (payload) => { + console.log('Media SDK:', payload); +}); + +client.on('device-change', () => { + console.log('Device changed'); +}); +``` + +## Common Methods + +### User Information + +```javascript +// Get current user +const currentUser = client.getCurrentUser(); +console.log('Current user:', currentUser); + +// Get all participants +const participants = client.getParticipantsList(); +console.log('Participants:', participants); + +// Check if user is host +const isHost = client.isHost(); +``` + +### Audio Control + +```javascript +// Mute/unmute self +await client.mute(true); // mute +await client.mute(false); // unmute + +// Mute/unmute specific user (host only) +await client.muteAudio(userId, true); + +// Mute all (host only) +await client.muteAllAudio(true); +``` + +### Video Control + +```javascript +// Start/stop video +await client.startVideo(); +await client.stopVideo(); + +// Mute/unmute user's video (host only) +await client.muteVideo(userId, true); +``` + +### Meeting Control + +```javascript +// Leave meeting +client.leaveMeeting(); + +// End meeting (host only) +client.endMeeting(); +``` + +### Screen Share + +```javascript +// Start screen share +await client.startShareScreen(); + +// Stop screen share +await client.stopShareScreen(); +``` + +### Recording + +```javascript +// Start recording (cloud) +await client.startCloudRecording(); + +// Stop recording +await client.stopCloudRecording(); +``` + +### Virtual Background + +```javascript +// Check support +const isSupported = await client.isSupportVirtualBackground(); + +// Set virtual background +await client.setVirtualBackground(imageUrl); + +// Remove virtual background +await client.removeVirtualBackground(); +``` + +### Rename + +```javascript +// Rename user +await client.rename(userId, 'New Name'); +``` + +## React Integration + +### Basic Pattern + +```tsx +import { useEffect, useRef, useState, useCallback } from 'react'; +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +type ZoomClient = ReturnType; + +function ZoomMeeting({ meetingNumber, password, userName }: Props) { + const clientRef = useRef(null); + const containerRef = useRef(null); + const [isJoined, setIsJoined] = useState(false); + const [error, setError] = useState(null); + + // Create client once + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); + + const joinMeeting = useCallback(async () => { + if (!clientRef.current || !containerRef.current) return; + + try { + // Get signature from backend + const { signature, sdkKey } = await fetchSignature(meetingNumber); + + await clientRef.current.init({ + zoomAppRoot: containerRef.current, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await clientRef.current.join({ + signature, + sdkKey, + meetingNumber, + password, + userName, + }); + + setIsJoined(true); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to join'); + } + }, [meetingNumber, password, userName]); + + return ( +
+
+ {!isJoined && ( + + )} + {error &&
{error}
} +
+ ); +} +``` + +### Event Handling in React + +```tsx +useEffect(() => { + if (!clientRef.current) return; + + const handleConnectionChange = (payload: any) => { + if (payload.state === 'Connected') { + setIsJoined(true); + } else if (payload.state === 'Closed') { + setIsJoined(false); + } + }; + + const handleUserAdded = (payload: any) => { + console.log('Users joined:', payload); + }; + + clientRef.current.on('connection-change', handleConnectionChange); + clientRef.current.on('user-added', handleUserAdded); + + return () => { + clientRef.current?.off('connection-change', handleConnectionChange); + clientRef.current?.off('user-added', handleUserAdded); + }; +}, []); +``` + +## Positioning and Resizing + +### Initial Size + +```javascript +await client.init({ + zoomAppRoot: element, + customize: { + video: { + viewSizes: { + default: { width: 1000, height: 600 } + } + } + } +}); +``` + +### Dynamic Resizing + +The container element size determines the meeting UI size. To resize: + +```javascript +// Just resize the container +document.getElementById('meetingSDKElement').style.width = '1200px'; +document.getElementById('meetingSDKElement').style.height = '800px'; +``` + +### Making it Resizable + +```javascript +customize: { + video: { + isResizable: true + } +} +``` + +## Supported Features + +Component View supports core meeting functionality. Some features from Client View may not be available. + +| Feature | Supported | +|---------|-----------| +| Audio/Video | ✅ | +| Screen Share | ✅ | +| Chat | ✅ | +| Virtual Background | ✅ | +| Breakout Rooms | ✅ | +| Cloud Recording | ✅ | +| Closed Captions | ✅ | +| Live Transcription | ✅ | +| Waiting Room | ✅ | +| Gallery View | ✅ | +| Reactions | ✅ | +| Raise Hand | ✅ | + +Contact Zoom Developer Support to request additional features. + +## Error Handling + +```javascript +try { + await client.join({ + // ... options + }); +} catch (error) { + // error.reason contains error code + // error.message contains description + + switch (error.reason) { + case 'WRONG_MEETING_PASSWORD': + console.error('Incorrect password'); + break; + case 'MEETING_NOT_START': + console.error('Meeting has not started'); + break; + case 'INVALID_PARAMETERS': + console.error('Invalid join parameters'); + break; + default: + console.error('Join failed:', error.message); + } +} +``` + +## Comparison with Client View + +| Feature | Component View | Client View | +|---------|----------------|-------------| +| **API Style** | Promises | Callbacks | +| **Password param** | `password` | `passWord` | +| **Container** | Custom element | Auto `#zmmtg-root` | +| **UI** | Embeddable | Full-page | +| **Preloading** | Not needed | `preLoadWasm()` | +| **Language** | Init option | `i18n.load()` | +| **Events** | `on()`/`off()` | `inMeetingServiceListener()` | + +## Resources + +- [Main Web SDK Skill](../SKILL.md) +- [Reference Index](references/README.md) +- [Error Codes](../troubleshooting/error-codes.md) +- [Common Issues](../troubleshooting/common-issues.md) +- [SharedArrayBuffer Setup](../concepts/sharedarraybuffer.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/references/README.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/references/README.md new file mode 100644 index 00000000..9fa22670 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/component-view/references/README.md @@ -0,0 +1,9 @@ +# Component View Reference Index + +Use this index when you need the stable entrypoints around Component View behavior before deeper per-method pages are expanded. + +- [Component View Skill](../SKILL.md) +- [SharedArrayBuffer Setup](../../concepts/sharedarraybuffer.md) +- [Error Codes](../../troubleshooting/error-codes.md) +- [Common Issues](../../troubleshooting/common-issues.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/browser-support.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/browser-support.md new file mode 100644 index 00000000..978ae78a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/browser-support.md @@ -0,0 +1,223 @@ +# Browser Support for Zoom Meeting SDK Web + +The Zoom Meeting SDK for Web supports browsers within **two versions** of their current release. This document details feature support by browser. + +## Feature Support Matrix + +| Feature | Chrome | Firefox | Safari | Edge | iOS/iPadOS | Android | +|---------|--------|---------|--------|------|------------|---------| +| **Video** | +| 720p Video (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| 720p Video (send) | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| 1080p (webinar attendees) | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| Gallery View (25 videos) | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | +| Virtual Background | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| WebRTC Video | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| **Audio** | +| Audio (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Audio (send) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Background Noise Suppression | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| Share Tab Audio | ✅¹ | ❌ | ❌ | ✅¹ | ❌ | ❌ | +| Call In (PSTN) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Call Out (PSTN) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Screen Sharing** | +| Screen Share (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Screen Share (send) | ✅ | ✅ | ✅³ | ✅ | ❌ | ❌ | +| Remote Control (give) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Be Remote Controlled | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Meeting Features** | +| Breakout Rooms | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Waiting Room | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| In-Meeting Chat | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Chat - Send File | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Cloud Recording | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Closed Captioning | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Live Transcription | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Live Translation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| RTMP Live Streaming | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Webinar Q&A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Whiteboard** | +| Whiteboard (view) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Whiteboard (edit) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **Other** | +| Stay Awake (Component View) | ✅⁴ | ❌ | ✅⁴ | ✅⁴ | ❌ | ❌ | +| Encryption (TLS 1.2 + AES-GCM) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| End-to-End Encryption (E2EE) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | + +### Footnotes + +1. **Requires SharedArrayBuffer** - Must enable COOP/COEP headers. See [sharedarraybuffer.md](sharedarraybuffer.md). +2. **Safari Gallery View** - Requires Safari 17.0+, macOS Sonoma, and SDK v2.18.0+. +3. **Safari Screen Share** - Component View supported. Client View requires Safari 17+ with macOS 14 Sonoma+. +4. **Stay Awake** - Chrome 116+, Edge 90+, Safari 16.4+. Uses [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/WakeLock). + +## Version Support Policy + +Zoom supports the current browser version plus two previous versions: + +| If Current Version Is | Supported Versions | +|-----------------------|-------------------| +| Chrome 140 | 138, 139, 140 | +| Firefox 130 | 128, 129, 130 | +| Safari 18 | 16, 17, 18 | +| Edge 130 | 128, 129, 130 | + +## Mobile and Tablet Browser Support + +### iOS and iPadOS + +All browsers on iOS/iPadOS use the same WebKit engine (including Chrome and Firefox on iOS). Features are determined by **iOS version**, not browser version. + +| iOS Version | Key Capabilities | +|-------------|------------------| +| iOS 15.2+ | SharedArrayBuffer support | +| iOS 16.4+ | 720p in landscape mode, WakeLock | +| iOS 17+ | Improved WebRTC performance | + +### Android + +Most Android browsers are Chromium-based. Features depend on **Android OS version** and **Chrome version**. + +| Android Version | Notes | +|-----------------|-------| +| Android 10+ | Full support | +| Chrome 112+ | 720p in landscape mode | + +**Important**: Android Firefox is NOT supported (uses GeckoView engine). + +### Samsung Internet + +Samsung Internet follows its [own versioning scheme](https://en.wikipedia.org/wiki/Samsung_Internet#History) but is Chromium-based: + +| Samsung Internet | Chromium Base | +|------------------|---------------| +| 20.0 | Chromium 106 | +| 21.0 | Chromium 111 | +| 22.0 | Chromium 118 | + +### Tablets + +| Device Type | Browser Support | +|-------------|-----------------| +| iPad | Same as iOS Safari | +| Android Tablets | Same as Android browsers | +| Microsoft Surface | Same as Windows desktop browsers | +| Chromebooks | Same as Chrome desktop | + +## WebRTC Support Details + +WebRTC video is supported on: + +- **Chrome**: Windows, macOS, Android, iOS, ChromeOS +- **Edge**: Windows +- **Safari**: macOS, iOS (WebKit-based browsers) + +Some Android device models have specific limitations due to hardware variations. + +## Content Security Policy (CSP) + +If you use Content Security Policy headers, configure them to allow Zoom SDK: + +``` +Content-Security-Policy: + default-src 'self'; + base-uri 'self'; + worker-src blob:; + style-src 'self' 'unsafe-inline'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://zoom.us *.zoom.us dmogdx0jrul3u.cloudfront.net blob:; + connect-src 'self' https://zoom.us https://*.zoom.us wss://*.zoom.us; + img-src 'self' https:; + media-src 'self' https:; + font-src 'self' https:; +``` + +### CSP Error: WebAssembly.instantiate() + +If you see this error: + +``` +CompileError: WebAssembly.instantiate(): Refused to compile... +``` + +Add `'unsafe-eval'` or `'wasm-unsafe-eval'` (if browser supports it) to your `script-src` directive. + +## Known Limitations + +### All Browsers +- **E2EE not supported** on any web browser +- **Cannot be remote-controlled** (browsers don't support this) + +### Safari +- No virtual background support +- Screen share (send) limited to Safari 17+ on macOS Sonoma +- No tab audio sharing + +### Firefox +- No WebRTC video (uses different implementation) +- No tab audio sharing +- No Stay Awake feature + +### Mobile (iOS/Android) +- No screen share sending +- No virtual backgrounds +- No whiteboard editing +- No Stay Awake feature +- No remote control + +## Checking Browser Compatibility + +### In Your Application + +```javascript +// Client View +const requirements = ZoomMtg.checkSystemRequirements(); +console.log('Browser compatible:', requirements.browserInfo); + +// Component View +const requirements = client.checkSystemRequirements(); +console.log('Video supported:', requirements.video); +console.log('Audio supported:', requirements.audio); +console.log('Screen share supported:', requirements.screen); +``` + +### Feature Detection + +```javascript +// Check SharedArrayBuffer for HD features +const hasHD = typeof SharedArrayBuffer === 'function'; + +// Check screen capture support +const hasScreenShare = navigator.mediaDevices && + typeof navigator.mediaDevices.getDisplayMedia === 'function'; + +// Check WebRTC support +const hasWebRTC = !!(window.RTCPeerConnection || + window.webkitRTCPeerConnection || + window.mozRTCPeerConnection); + +// Check WakeLock support (Stay Awake) +const hasWakeLock = 'wakeLock' in navigator; +``` + +## Recommendations + +### For Maximum Compatibility +1. Use Chrome or Edge on desktop +2. Enable SharedArrayBuffer headers for HD features +3. Test on iOS Safari for mobile users +4. Provide fallback messaging for unsupported features + +### For Virtual Backgrounds +- Require Chrome, Edge, or Firefox (desktop only) +- Ensure SharedArrayBuffer is enabled +- Safari users won't have this feature + +### For Screen Sharing +- Desktop browsers only +- Safari users need macOS Sonoma + Safari 17+ for Client View +- Component View works on Safari with earlier versions + +## Official Resources + +- [Zoom Web Client Features](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0064261) +- [Zoom Platform Comparison](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0065520) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/sharedarraybuffer.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/sharedarraybuffer.md new file mode 100644 index 00000000..6abfafc5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/concepts/sharedarraybuffer.md @@ -0,0 +1,310 @@ +# SharedArrayBuffer for Zoom Meeting SDK Web + +SharedArrayBuffer (SAB) is a web API that enables shared memory in JavaScript, significantly improving performance for WebAssembly-based features. Zoom uses SharedArrayBuffer to power advanced features. + +## Why SharedArrayBuffer Matters + +**Features that REQUIRE SharedArrayBuffer:** +- 720p video sending (HD) +- 1080p video for webinar attendees +- Gallery view (up to 25 videos) +- Virtual backgrounds +- Background noise suppression +- Share tab audio (Chrome/Edge) + +**Without SharedArrayBuffer:** +- Video limited to standard definition +- Gallery view may show fewer participants +- Virtual backgrounds unavailable +- Some performance features degraded + +> **Note**: SharedArrayBuffer is NOT required for basic meeting functionality or WebRTC. The SDK will work without it, but with limited features. + +## How to Enable SharedArrayBuffer + +SharedArrayBuffer requires **Cross-Origin Isolation**. You must configure your server to send specific HTTP headers. + +### Quick Comparison of Methods + +| Method | Type | Custom Headers Required | Best For | +|--------|------|------------------------|----------| +| **Cross-Origin Isolation** | Permanent | Yes | Production | +| **Credentialless Headers** | Permanent | Yes | Production with 3rd-party content | +| **Document-Isolation-Policy** | Permanent | Yes | Chrome/Edge 137+ with iframes | +| **Service Workers** | Permanent | No | GitHub Pages, static hosts | +| **Chrome Origin Trials** | Temporary | No | Testing only (renew every 3 months) | + +## Implementation Methods + +### Method 1: Cross-Origin Isolation (Recommended) + +Add these headers to ALL responses from your server: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +**Pros**: Industry standard, works across all browsers +**Cons**: May break third-party iframes/content without CORS headers + +### Method 2: Credentialless Headers + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: credentialless +``` + +**Pros**: More compatible with third-party content +**Cons**: Same browser support as Method 1 + +### Method 3: Document-Isolation-Policy (Chrome/Edge 137+) + +``` +Document-Isolation-Policy: isolate-and-require-corp +# OR +Document-Isolation-Policy: isolate-and-credentialless +``` + +**Pros**: Allows embedding third-party iframes, videos, payment gateways +**Cons**: Chrome/Edge 137+ only (desktop) + +### Method 4: Service Worker (No Server Headers) + +For platforms that don't allow custom headers (GitHub Pages): + +1. Add [coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) to your project: + +```html + + +``` + +2. Place `coi-serviceworker.js` in your root directory + +**Pros**: Works on static hosting without header control +**Cons**: Adds slight overhead, requires service worker support + +### Method 5: Chrome Origin Trials (Temporary) + +1. Register at [Chrome Origin Trials](https://developer.chrome.com/origintrials/#/trials/active) +2. Add meta tag to your page: + +```html + +``` + +**Pros**: Quick testing without server changes +**Cons**: Must renew every 3 months, will be deprecated + +## Platform-Specific Configuration + +### Vercel + +**Next.js (next.config.js):** +```javascript +module.exports = { + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + { + key: 'Cross-Origin-Embedder-Policy', + value: 'require-corp', + }, + ], + }, + ]; + }, +}; +``` + +**Non-Next.js (vercel.json):** +```json +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, + { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" } + ] + } + ] +} +``` + +### Netlify (_headers file) + +Create `_headers` in your publish directory: + +``` +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp +``` + +### AWS CloudFront + +Use CloudFront Response Headers Policy: + +1. Go to CloudFront > Policies > Response headers +2. Create custom policy with: + - `Cross-Origin-Opener-Policy: same-origin` + - `Cross-Origin-Embedder-Policy: require-corp` +3. Attach to your distribution + +### Google App Engine (app.yaml) + +```yaml +handlers: + - url: /.* + static_files: index.html + upload: index.html + http_headers: + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp +``` + +### nginx + +```nginx +server { + location / { + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + # ... other config + } +} +``` + +### Apache (.htaccess) + +```apache + + Header set Cross-Origin-Opener-Policy "same-origin" + Header set Cross-Origin-Embedder-Policy "require-corp" + +``` + +### Express.js + +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +### GitHub Pages + +GitHub Pages doesn't support custom headers. Use the **Service Worker method**: + +1. Download [coi-serviceworker.js](https://github.com/gzuidhof/coi-serviceworker) +2. Place in repository root +3. Add to your HTML before other scripts: + +```html + +``` + +## Verify SharedArrayBuffer is Enabled + +### In Browser Console + +```javascript +// Check if SharedArrayBuffer is available +console.log('SharedArrayBuffer:', typeof SharedArrayBuffer === 'function'); + +// Check cross-origin isolation +console.log('Cross-Origin Isolated:', window.crossOriginIsolated); +``` + +### In Your Application + +```javascript +// Before initializing Zoom SDK +if (typeof SharedArrayBuffer !== 'function') { + console.warn('SharedArrayBuffer not available. HD features will be limited.'); + console.warn('Enable COOP/COEP headers on your server.'); +} + +// Check isolation status +if (!window.crossOriginIsolated) { + console.warn('Page is not cross-origin isolated.'); +} +``` + +### Using Zoom SDK + +```javascript +// Client View - use disableCORP for development without headers +ZoomMtg.init({ + disableCORP: !window.crossOriginIsolated, + // ... other options +}); +``` + +## Troubleshooting + +### "SharedArrayBuffer is not defined" + +**Cause**: Headers not configured or browser doesn't support cross-origin isolation. + +**Fix**: +1. Verify headers are being sent (check Network tab in DevTools) +2. Ensure headers are on ALL responses (not just HTML) +3. Check for conflicting headers from CDN/proxy + +### Third-Party Content Blocked + +**Cause**: `require-corp` blocks resources without CORS headers. + +**Fix**: +1. Use `credentialless` instead of `require-corp` +2. Or add `crossorigin="anonymous"` to external resources: + ```html + + ``` + +### Service Worker Not Working + +**Cause**: Service worker not registered or not at root. + +**Fix**: +1. Ensure `coi-serviceworker.js` is in the root directory +2. Check service worker is registered in DevTools > Application > Service Workers +3. Try hard refresh (Ctrl+Shift+R) + +### Headers Present but SAB Still Unavailable + +**Cause**: Possibly Chrome < 92 or other browser limitations. + +**Fix**: +1. Update browser to latest version +2. Check if browsing in incognito (some extensions can interfere) +3. Verify both COOP and COEP headers are present + +## Browser Support for SharedArrayBuffer + +| Browser | Version | Notes | +|---------|---------|-------| +| Chrome | 92+ | Full support with COOP/COEP | +| Edge | 92+ | Full support with COOP/COEP | +| Firefox | 79+ | Full support with COOP/COEP | +| Safari | 15.2+ | Full support with COOP/COEP | +| iOS Safari | 15.2+ | Full support with COOP/COEP | +| Android Chrome | 92+ | Full support with COOP/COEP | + +## Additional Resources + +- [Why you need "cross-origin isolated" for powerful features](https://web.dev/articles/why-coop-coep) +- [Making your website "cross-origin isolated"](https://web.dev/articles/coop-coep) +- [coi-serviceworker GitHub](https://github.com/gzuidhof/coi-serviceworker) +- [Document-Isolation-Policy explainer](https://github.com/nickreese/nickreese.github.io/tree/main/document-isolation-policy) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/client-view-basic.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/client-view-basic.md new file mode 100644 index 00000000..7f953c37 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/client-view-basic.md @@ -0,0 +1,12 @@ +# Client View (Basic) Example + +Minimal "get it to join" shape for Client View. Use it when you want the smallest possible starting point before layering in more configuration. + +## When To Use + +- You want the classic Zoom Web Client look. +- You are OK with callback-style APIs and less UI customization. + +## Next + +- Read `../client-view/SKILL.md` for the full integration and config knobs. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/component-view-react.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/component-view-react.md new file mode 100644 index 00000000..e67c357d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/examples/component-view-react.md @@ -0,0 +1,12 @@ +# Component View (React) Example + +Minimal "get it to join" shape for Component View in a React app. Use it as a starting point before adding more layout, state, and event handling. + +## When To Use + +- You want an embeddable meeting experience inside your UI. +- You want Promise-based APIs and more layout control. + +## Next + +- Read `../component-view/SKILL.md` for the full integration and event patterns. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-breakout-rooms.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-breakout-rooms.md new file mode 100644 index 00000000..18fc5d23 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-breakout-rooms.md @@ -0,0 +1,37 @@ +--- +title: "Web Component View: Breakout Rooms (What’s Possible)" +--- + +# Web Component View: Breakout Rooms (What’s Possible) + +This targets questions like: “How do I create breakout rooms in Component View?” + +## First: Confirm Context + +- Are you **host** or **participant**? +- Are you using **Client View** (`ZoomMtg`) or **Component View** (`ZoomMtgEmbedded`)? + +## Reality Check + +Breakout rooms are a host-controlled feature. Even when the SDK UI supports breakout rooms, programmatic creation/open/close often requires: + +- correct role (host/co-host) +- the meeting to have breakout rooms enabled +- supported APIs for the view type you’re using + +## Where to Look Next + +- Feature support table and Component View reference: + - `meeting-sdk/web/component-view/SKILL.md` +- Client View examples often show breakout room API methods on `ZoomMtg`: + - `meeting-sdk/web/client-view/SKILL.md` +- Cross-platform breakout room notes: + - `meeting-sdk/references/breakout-rooms.md` + +## Recommended Answer Pattern + +1. If the user is on Component View: + - confirm whether they’re trying to automate breakout rooms or just use the built-in UI +2. If they need automation: + - identify the exact SDK version and view + - confirm whether the needed API exists for that view (don’t assume parity) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-ui-customization.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-ui-customization.md new file mode 100644 index 00000000..604f4e2a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/component-view-ui-customization.md @@ -0,0 +1,56 @@ +--- +title: "Web Component View: UI Customization and Limitations" +--- + +# Web Component View: UI Customization and Limitations + +This addresses common questions like: + +- “How do I hide meeting info / passcode / invite URL?” +- “How do I remove Participants / Audio / Video / Share buttons?” +- “Can I fully customize the Meeting SDK UI?” + +## First: Confirm View Type + +- **Component View** (npm): `ZoomMtgEmbedded.createClient()` +- **Client View** (CDN): `ZoomMtg.*` + +The available customization knobs differ. + +## Component View Customization Entry Point + +Component View customization is done via `client.init({ customize: { ... } })`. + +The most common knob is `customize.meetingInfo`, which controls what shows in “meeting info” surfaces. + +Example reference: +- `meeting-sdk/web/component-view/SKILL.md` (Customize Object section) +- `meeting-sdk/web/SKILL.md` (Key options snippet) + +## Hiding “Meeting Info” (Topic/MN/Passcode/Invite) + +Use `customize.meetingInfo` to control which fields are displayed. + +Important: this controls what the **SDK UI displays**. It does not change the meeting’s underlying security settings. + +## Removing Default Buttons (Participants/Audio/Video/Share) + +Forum expectation mismatch: the SDK supports **adding** custom toolbar buttons, but “removing all built-in controls” is not always supported (and CSS-hacking the Zoom UI is brittle and can break across SDK updates). + +Recommended answer pattern: + +1. Ask which exact controls they want removed and why (UX vs compliance). +2. If it’s a compliance/policy requirement (e.g. “prevent recording/screen capture”), treat it as **not solvable purely via SDK UI**. +3. If it’s UX: + - prefer supported `customize` options + - otherwise, set expectations about what is and isn’t controllable + +## Don’t Use CSS Hacks as a Primary Strategy + +You *can* sometimes hide elements with CSS selectors, but: +- it is fragile +- it can break accessibility/flow +- it may violate intended product behavior + +Use official knobs first. + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md new file mode 100644 index 00000000..5063ba4d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md @@ -0,0 +1,32 @@ +--- +title: "Web: SharedArrayBuffer and Gallery View" +--- + +# Web: SharedArrayBuffer and Gallery View + +Many Meeting SDK Web issues that mention gallery view or performance are caused by missing cross-origin isolation. + +## What to Ask + +- Are they using **Client View** (CDN) or **Component View** (npm)? +- Is the app served over **HTTPS**? +- Does the browser report `crossOriginIsolated === true`? + +## Symptoms + +- “SharedArrayBuffer is not defined” +- “Your browser doesn’t support gallery view” +- performance degradation on certain machines/browsers + +## What to Do + +1. Serve the app with the required COOP/COEP headers so `crossOriginIsolated` can become true. +2. Avoid embedding the app in contexts that break isolation (some iframes/proxies). +3. If the environment cannot be isolated (enterprise proxy, incompatible embeds), treat gallery view / HD features as best-effort and fall back gracefully. + +## Debug Checklist + +- In DevTools console: + - `window.crossOriginIsolated` should be `true` for SAB-dependent features. +- Verify required headers are present on **all** relevant responses (HTML + JS + WASM). + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-performance-cpu.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-performance-cpu.md new file mode 100644 index 00000000..276fec9c --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-performance-cpu.md @@ -0,0 +1,26 @@ +--- +title: "Web: Performance and CPU (Meeting SDK)" +--- + +# Web: Performance and CPU (Meeting SDK) + +Common “meeting web” questions boil down to resource usage and rendering constraints. + +## Clarify the Integration + +- Client View vs Component View +- Expected participant count and whether gallery view is required +- Target devices (low-end laptops, thin clients, mobile browsers) + +## General Levers + +- Prefer realistic expectations: browsers + multi-video rendering are CPU-heavy. +- Avoid extra DOM/layout work around the meeting container. +- Ensure SharedArrayBuffer / cross-origin isolation is configured when required (see `sharedarraybuffer-gallery-view.md`). + +## Practical Checks + +- Confirm the meeting container is not constantly re-rendering (React state loops around the Zoom root). +- Check for global CSS resets that impact layout and cause expensive reflows. +- On constrained machines, reduce unnecessary UI overlays and avoid heavy background effects. + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md new file mode 100644 index 00000000..ac530491 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md @@ -0,0 +1,33 @@ +--- +title: "Web: Joining Meeting Timeout / Browser Restriction" +--- + +# Web: Joining Meeting Timeout / Browser Restriction + +Forum threads often report errors like: + +- “Joining meeting timeout” +- “Your network connection has timed out” +- “Your organization has disabled access to Zoom from the browser” + +## What to Ask (Quick) + +- Browser + OS +- Are they behind a corporate proxy/VPN/firewall? +- Does the issue reproduce on a clean network (mobile hotspot)? +- Are any ad blockers/privacy tools enabled? +- Client View (CDN `ZoomMtg`) vs Component View (npm `ZoomMtgEmbedded`) + +## Common Root Causes + +- Network blocks: WebSocket / media / Zoom domains blocked by org policy +- CSP/proxy rewriting that breaks SDK resources +- Mixed content / non-HTTPS in dev environments + +## Practical Debug Steps + +1. Try from a different network (hotspot) to isolate policy blocks quickly. +2. Disable ad blockers/privacy extensions for the site. +3. Verify the page and SDK assets load without 4xx/5xx in DevTools Network tab. +4. If in enterprise environment: ask for the organization’s allowlist policy for Zoom web traffic. + diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-tracking-id.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-tracking-id.md new file mode 100644 index 00000000..66ece08f --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web-tracking-id.md @@ -0,0 +1,57 @@ +# Meeting SDK Web - Tracking ID + +Use tracking IDs to identify users across sessions. + +## Overview + +Tracking IDs allow you to identify users joining meetings from your application for analytics and tracking purposes. + +## Setting Tracking ID + +### Component View + +```javascript +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User Name', + trackingId: 'your-tracking-id-here' +}); +``` + +### Client View + +```javascript +ZoomMtg.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User Name', + trackingId: 'your-tracking-id-here' +}); +``` + +## Use Cases + +- Analytics and reporting +- User identification across meetings +- Integration with your user database +- Conversion tracking + +## Tracking ID in Reports + +Tracking IDs appear in: +- Meeting participant reports +- Dashboard analytics +- Webhook payloads + +## Best Practices + +1. Use consistent IDs across sessions +2. Don't include sensitive data in tracking IDs +3. Document your tracking ID schema + +## Resources + +- **Meeting SDK Web docs**: https://developers.zoom.us/docs/meeting-sdk/web/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web.md new file mode 100644 index 00000000..c5751836 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/references/web.md @@ -0,0 +1,309 @@ +# Meeting SDK - Web + +Embed Zoom meetings in web applications. + +## Overview + +The Zoom Meeting SDK for Web embeds the full Zoom meeting experience into your web application with two view options: Component View and Client View. + +## Forum-Derived Hotspots + +- SharedArrayBuffer / gallery view: [sharedarraybuffer-gallery-view.md](sharedarraybuffer-gallery-view.md) +- Joining meeting timeout / browser restriction: [web-timeout-browser-restriction.md](web-timeout-browser-restriction.md) +- Performance/CPU guidance: [web-performance-cpu.md](web-performance-cpu.md) +- Component View UI customization: [component-view-ui-customization.md](component-view-ui-customization.md) +- Component View breakout rooms: [component-view-breakout-rooms.md](component-view-breakout-rooms.md) + +## Prerequisites + +- Meeting SDK credentials from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- SDK Key and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) + +## View Options + +| View | Description | Use Case | +|------|-------------|----------| +| **Component View** | Flexible, embeddable UI components | Custom layouts, React/Vue integration | +| **Client View** | Full-page Zoom meeting experience | Quick integration, standard Zoom UI | + +## Implementation Approaches + +| Approach | Technology | Port | View | Best For | +|----------|-----------|------|------|----------| +| **Components** | React + TypeScript + Vite | 3000 | Component | Modern, flexible integration | +| **Local** | React + Webpack + NPM | 9999 | Client | Traditional npm-based setup | +| **CDN** | Vanilla JS + CDN | 9999 | Client | Simple, no build tools | + +## Installation + +### Component View (Recommended) + +```bash +npm install @zoom/meetingsdk +``` + +### Client View (CDN) + +```html + + + + + + +``` + +> **Note:** CDN provides `ZoomMtg` (Client View). For `ZoomMtgEmbedded` (Component View), use npm. + +### Auth Endpoint (Required) + +The Meeting SDK requires a signature from an authentication backend: + +```bash +# Clone Zoom's auth endpoint sample +git clone https://github.com/zoom/meetingsdk-auth-endpoint-sample --depth 1 +cd meetingsdk-auth-endpoint-sample +cp .env.example .env +# Edit .env with your SDK credentials +npm install && npm run start +``` + +## Quick Start (Component View) + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User', + password: password, +}); +``` + +## WebRTC Optimizations + +Meeting SDK Web uses WebRTC for real-time communication with optimizations for: +- HD video (720p/1080p) +- Low latency audio +- Adaptive bitrate + +### Codec Support +- H.264 for video +- VP8 as fallback +- Opus for audio + +## HD Video + +### Check System Requirements + +```javascript +// Check browser compatibility +const compatibility = client.checkSystemRequirements(); +console.log('Video supported:', compatibility.video); +console.log('Audio supported:', compatibility.audio); +console.log('Screen share supported:', compatibility.screen); +``` + +### Check SharedArrayBuffer (Required for HD) + +```javascript +// SharedArrayBuffer is REQUIRED for 720p, gallery view, virtual background +const sabAvailable = typeof SharedArrayBuffer === 'function'; + +if (!sabAvailable) { + console.warn('HD features require SharedArrayBuffer'); + console.warn('Enable COOP/COEP headers on your server'); +} +``` + +### Enable HD in Init + +```javascript +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + + // Enable 720p (default true for SDK >= 2.8.0) + enableHD: true, + + // Enable 1080p for webinar attendees (SDK >= 2.9.0) + enableFullHD: true, +}); +``` + +### HD Requirements + +To enable 720p: +1. Contact Zoom Support to enable on your account +2. Enable "Group HD" in Zoom profile settings +3. SharedArrayBuffer must be available (COOP/COEP headers) +4. Solid internet connection and low CPU usage + +**Resolution tiers:** +- 1:1 calls: Up to 1080p +- Small groups (2-4): Up to 720p +- Larger meetings: Adaptive + +**Key limitation:** If a 3rd participant turns video on, quality reverts to standard definition. + +## SharedArrayBuffer + +For optimal performance, configure these headers: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +## Event Handling + +### Component View Events + +```javascript +client.on('connection-change', (payload) => { + console.log('Connection state:', payload.state); +}); + +client.on('user-added', (payload) => { + console.log('User joined:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('User left:', payload); +}); +``` + +### Client View In-Meeting Listeners + +```javascript +// User events +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); +}); + +// Waiting room +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); + +// Meeting status changes +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + console.log('Meeting status:', data); +}); +``` + +## Common Tasks + +### Join Meeting +```javascript +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User', +}); +``` + +### Leave Meeting +```javascript +client.leaveMeeting(); +``` + +## Client View Full Example + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Check system requirements +console.log(ZoomMtg.checkSystemRequirements()); + +// Preload WebAssembly for faster init +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Initialize and join +ZoomMtg.init({ + leaveUrl: '/meeting-ended', + disableCORP: !window.crossOriginIsolated, // Disable if no COOP/COEP + success: () => { + ZoomMtg.join({ + meetingNumber: '123456789', + userName: 'User Name', + signature: signature, // From auth endpoint + userEmail: 'user@example.com', + passWord: 'meeting-password', + success: (res) => { + console.log('Joined meeting'); + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => console.error('Join error:', err) + }); + }, + error: (err) => console.error('Init error:', err) +}); +``` + +## China CDN + +For users in China, use the China-specific CDN: + +```javascript +// Set before preLoadWasm() +ZoomMtg.setZoomJSLib('https://jssdk.zoomus.cn/{VERSION}/lib', '/av'); +``` + +## Zoom for Government (ZFG) + +For government applications, apply for SDK credentials at [ZFG Marketplace](https://marketplace.zoomgov.com/). + +### Option 1: ZFG-specific NPM Package + +```json +{ + "dependencies": { + "@zoom/meetingsdk": "3.11.2-zfg" + } +} +``` + +### Option 2: Configure ZFG Endpoints + +**Client View:** +```javascript +ZoomMtg.setZoomJSLib('https://source.zoomgov.com/{VERSION}/lib', '/av'); +ZoomMtg.init({ + webEndpoint: 'www.zoomgov.com', +}); +``` + +**Component View:** +```javascript +const client = ZoomMtgEmbedded.createClient(); +client.init({ + assetPath: 'https://source.zoomgov.com/{VERSION}/lib/av', + webEndpoint: 'www.zoomgov.com' +}); +``` + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Component View sample**: https://github.com/zoom/meetingsdk-web-sample +- **Client View sample**: https://github.com/zoom/meetingsdk-sample-signature-node.js diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..222f3eec --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/common-issues.md @@ -0,0 +1,471 @@ +# Common Issues - Zoom Meeting SDK Web + +Quick diagnostics and solutions for the most common issues. + +## Quick Diagnostic Workflow + +``` +1. Check browser console for errors +2. Verify signature is valid and not expired +3. Check COOP/COEP headers for HD features +4. Verify SDK version is supported +5. Test in Chrome/Edge first (most compatible) +``` + +## Initialization Issues + +### "Meeting not initialized" (Error 2) + +**Symptom**: SDK throws error when trying to join. + +**Cause**: `join()` called before `init()` completed. + +**Solution**: +```javascript +// WRONG +ZoomMtg.init({ leaveUrl: '...' }); +ZoomMtg.join({ ... }); // Too early! + +// CORRECT +ZoomMtg.init({ + leaveUrl: '...', + success: () => { + ZoomMtg.join({ ... }); // Wait for success callback + } +}); +``` + +### SDK Not Loading (CDN) + +**Symptom**: `ZoomMtg is not defined` + +**Cause**: Scripts not loaded in correct order. + +**Solution**: +```html + + + + + + + +``` + +### Language Loading Timeout + +**Symptom**: SDK hangs or UI shows wrong language. + +**Cause**: `init()` called before language loaded. + +**Solution**: +```javascript +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + // ONLY init after language is loaded + ZoomMtg.init({ ... }); +}); +``` + +## Authentication Issues + +### "Signature is invalid" (Error 3712) + +**Symptom**: Join fails with signature error. + +**Causes & Solutions**: + +1. **Wrong SDK Secret** + ```bash + # Verify in Zoom Marketplace > App > App Credentials + ``` + +2. **Signature expired** + ```javascript + // Check signature expiration (default 2 hours) + // Regenerate signature if needed + ``` + +3. **Missing appKey prefix (v5.0.0+)** + ```javascript + // WRONG (pre-5.0 format) + signature: "eyJhbGc..." + + // CORRECT (5.0+ format) + signature: "appKey:sdkKey.eyJhbGc..." + ``` + +4. **Wrong algorithm** + ```javascript + // MUST use HS256 + jwt.sign(payload, secret, { algorithm: 'HS256' }); + ``` + +### "API Key is invalid" (Error 3704) + +**Symptom**: SDK Key rejected. + +**Causes**: +1. Typo in SDK Key +2. SDK Key from different app type +3. SDK Key not activated + +**Solution**: Verify SDK Key in Zoom Marketplace matches exactly. + +### "SDK Key is disabled" (Error 3710) + +**Symptom**: Previously working key now fails. + +**Cause**: App deactivated in Marketplace. + +**Solution**: +1. Go to Zoom Marketplace > Manage > Your Apps +2. Re-enable or create new app + +## Join Issues + +### "passWord" vs "password" Typo + +**Symptom**: Join fails with password error even with correct password. + +**Cause**: Different spelling between views! + +**Solution**: +```javascript +// Client View - capital W +ZoomMtg.join({ + passWord: 'meeting123', // Capital W! +}); + +// Component View - lowercase +client.join({ + password: 'meeting123', // lowercase! +}); +``` + +### "Meeting does not exist" (Error 3001/3610) + +**Symptom**: Valid meeting number rejected. + +**Causes & Solutions**: + +1. **Wrong meeting number** + - Check for typos + - Use the 9-11 digit number, not Meeting ID from API + +2. **Meeting deleted or expired** + - Create new meeting + +3. **Meeting not started yet** + - Wait for host or enable "join before host" + +### "Wrong meeting password" (Error 3004) + +**Symptom**: Correct password rejected. + +**Causes**: +1. Space/encoding issues in password +2. Password changed after you got it +3. Using URL-encoded password directly + +**Solution**: +```javascript +// Extract password correctly from invite link +const url = new URL(inviteLink); +const password = url.searchParams.get('pwd'); +``` + +### "Another meeting running" (Error 3005) + +**Symptom**: Can't join new meeting. + +**Cause**: User already in another SDK meeting instance. + +**Solution**: +```javascript +// Leave current meeting first +ZoomMtg.leaveMeeting({}); +// Then join new meeting +``` + +## HD Video Issues + +### No HD Video / Low Quality + +**Symptom**: Video stuck at low resolution. + +**Cause**: SharedArrayBuffer not available. + +**Diagnostic**: +```javascript +console.log('Cross-origin isolated:', window.crossOriginIsolated); +console.log('SharedArrayBuffer:', typeof SharedArrayBuffer === 'function'); +``` + +**Solution**: Add COOP/COEP headers: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +See [concepts/sharedarraybuffer.md](../concepts/sharedarraybuffer.md) for details. + +### Virtual Background Not Working + +**Symptom**: Virtual background option missing or grayed out. + +**Causes**: +1. SharedArrayBuffer not available +2. Browser not supported (Safari, iOS, Android) +3. Hardware limitations + +**Solution**: +```javascript +// Check support first +ZoomMtg.isSupportVirtualBackground({ + success: (data) => { + if (data.result.isSupport) { + // VB supported + } else { + console.log('VB not supported:', data.result.reason); + } + } +}); +``` + +## Event Listener Issues + +### Callbacks Not Firing + +**Symptom**: `inMeetingServiceListener` events never trigger. + +**Causes & Solutions**: + +1. **Registered too late** + ```javascript + // Register BEFORE or AFTER init, but make sure SDK is ready + ZoomMtg.inMeetingServiceListener('onUserJoin', callback); + ``` + +2. **Wrong event name** + ```javascript + // Event names are case-sensitive + 'onUserJoin' // Correct + 'OnUserJoin' // Wrong + 'on-user-join' // Wrong + ``` + +3. **Meeting not fully joined** + ```javascript + // Wait for join success before expecting events + ZoomMtg.join({ + success: () => { + // Now events will fire + } + }); + ``` + +### Component View Events Not Firing + +**Symptom**: `client.on()` callbacks never trigger. + +**Solution**: +```javascript +// Component View uses different event names +client.on('connection-change', callback); // Not 'onMeetingStatus' +client.on('user-added', callback); // Not 'onUserJoin' +client.on('user-removed', callback); // Not 'onUserLeave' +``` + +## Browser-Specific Issues + +### Safari Screen Share Not Working + +**Symptom**: Screen share option missing on Safari. + +**Cause**: Requires Safari 17+ with macOS Sonoma for Client View. + +**Solution**: +- Use Component View (works with earlier Safari) +- Or instruct users to use Chrome/Edge + +### Firefox WebRTC Issues + +**Symptom**: Video issues on Firefox. + +**Cause**: Firefox uses different WebRTC implementation. + +**Solution**: Test in Chrome first, then adapt for Firefox. + +### Mobile Browser Limitations + +**Symptom**: Features missing on mobile. + +**Reality**: These features are NOT supported on mobile browsers: +- Screen share (send) +- Virtual backgrounds +- Whiteboard editing +- Remote control + +**Solution**: Detect mobile and adjust UI accordingly: +```javascript +const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); +if (isMobile) { + // Hide unsupported feature buttons +} +``` + +## CORS Issues + +### "Blocked by CORS policy" + +**Symptom**: SDK resources blocked. + +**Solution 1**: Use helper.html +```javascript +ZoomMtg.init({ + helper: './helper.html', + // ... +}); +``` + +**Solution 2**: Configure CSP headers +``` +Content-Security-Policy: + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://zoom.us *.zoom.us blob:; + connect-src 'self' https://zoom.us https://*.zoom.us wss://*.zoom.us; +``` + +### WebAssembly CORS Error + +**Symptom**: "Failed to load WebAssembly module" + +**Cause**: WASM files blocked by CSP. + +**Solution**: Add `wasm-unsafe-eval` or `unsafe-eval` to script-src: +``` +script-src 'self' 'wasm-unsafe-eval' ... +``` + +## React Integration Issues + +### Client Recreated on Every Render + +**Symptom**: Multiple SDK instances, memory leaks. + +**Cause**: `createClient()` in component body. + +**Solution**: +```javascript +// WRONG +function App() { + const client = ZoomMtgEmbedded.createClient(); // Created every render! +} + +// CORRECT +function App() { + const clientRef = useRef(null); + + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); +} +``` + +### "Cannot read property of null" on Container + +**Symptom**: Error when initializing Component View. + +**Cause**: Container element not ready. + +**Solution**: +```javascript +// WRONG +await client.init({ + zoomAppRoot: document.getElementById('meeting'), // Might be null +}); + +// CORRECT (React) +const containerRef = useRef(null); + +useEffect(() => { + if (containerRef.current) { + client.init({ zoomAppRoot: containerRef.current }); + } +}, []); + +return
; +``` + +## Performance Issues + +### Slow Join Time + +**Causes & Solutions**: + +1. **Not preloading WASM** + ```javascript + // Call early, before user clicks join + ZoomMtg.preLoadWasm(); + ZoomMtg.prepareWebSDK(); + ``` + +2. **Network latency** + - Use China CDN for China users + - Self-host assets with `assetPath` + +3. **Large bundle** + - Use code splitting + - Lazy load SDK + +### Memory Leaks + +**Symptom**: Browser memory grows over time. + +**Causes**: +1. Multiple SDK instances +2. Not cleaning up on unmount +3. Event listeners not removed + +**Solution**: +```javascript +ZoomMtg.init({ + leaveOnPageUnload: true, // Auto cleanup +}); +``` + +## Debug Mode + +### Enable Debug Logging + +**Client View**: +```javascript +ZoomMtg.init({ + debug: true, // Logs to console +}); +``` + +**Component View**: +```javascript +client.init({ + debug: true, +}); +``` + +### Mobile Debugging + +```javascript +// Use vConsole for mobile debugging +if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) { + const vConsole = new VConsole(); +} +``` + +## Getting Help + +1. **Check error codes**: [troubleshooting/error-codes.md](error-codes.md) +2. **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +3. **Developer forum**: https://devforum.zoom.us/ +4. **GitHub issues**: https://github.com/zoom/meetingsdk-web-sample/issues diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/error-codes.md b/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/error-codes.md new file mode 100644 index 00000000..7837eb6c --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/web/troubleshooting/error-codes.md @@ -0,0 +1,275 @@ +# Zoom Meeting SDK Web - Error Codes + +Comprehensive reference for all error codes returned by the Zoom Meeting SDK for Web. + +## Quick Lookup + +| Code Range | Category | +|------------|----------| +| 0-2 | General/Success | +| 3000-3999 | Meeting Validation | +| 4000-4999 | Connection Status | +| 6000+ | System/Service | +| 10000+ | SDK Version | +| 13000+ | Simulive | + +## General Errors (0-2) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `0` | SUCCESS | Function invoked successfully | N/A | +| `1` | FAIL | General function error | Check parameters and SDK state | +| `2` | MEETING_NOT_INIT | Meeting not initialized | Call `init()` before `join()` | + +## Meeting Validation Errors (3000-3999) + +### Authentication Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3704` | API_KEY_INVALID | SDK Key/Client ID is invalid | Verify SDK credentials in Marketplace | +| `3705` | SIGNATURE_EXPIRED | JWT signature has expired | Generate new signature with valid `exp` | +| `3708` | ROLE_ERROR | Incorrect role in signature | Use role 0 (participant) or 1 (host) | +| `3710` | API_KEY_DISABLED | SDK Key is deactivated | Re-enable in Marketplace or create new app | +| `3712` | SIGNATURE_INVALID | Signature verification failed | Check SDK secret, verify signature generation | +| `3265` | TOKEN_ERROR | Token validation failed | Check ZAK/OBF token format and expiry | +| `3623` | TOKEN_ERROR_ALT | Token error (alternate) | Same as 3265 | +| `3713` | NO_PERMISSION | Insufficient permissions | Verify account permissions and scopes | + +### Meeting Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3001` | ERROR_NOT_EXIST | Meeting does not exist | Verify meeting number | +| `3003` | ERROR_NOT_HOST | Not meeting host | Use host's ZAK token to start | +| `3004` | WRONG_MEETING_PASSWORD | Incorrect password | Verify `passWord` (Client View) or `password` (Component View) | +| `3005` | ANOTHER_MEETING_RUNNING | Already in another meeting | Leave current meeting first | +| `3008` | MEETING_NOT_START | Meeting hasn't started | Wait for host or use "join before host" | +| `3009` | BE_REMOVED | User was removed from meeting | Cannot rejoin; contact host | +| `3610` | MEETING_NOT_EXIST_ALT | Meeting does not exist (alt) | Same as 3001 | + +### Registration & Login + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3000` | EMAIL_REQUIRED | Email required for webinar | Provide `userEmail` in join params | +| `3099` | REGISTRATION_REQUIRED | Meeting requires registration | Get `tk` token from registration API | +| `3100` | LOGIN_REQUIRED | Zoom login required | Provide ZAK token for authenticated join | +| `3624` | HOST_EMAIL_REQUIRED | Host/alt host needed for webinar | Use host credentials to start | + +### Host Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3625` | HOST_INACTIVE | Meeting host is inactive | Contact host to activate account | +| `3702` | HOST_NOT_FOUND | Host does not exist | Verify host account | +| `3709` | HOST_NOT_FOUND | Host not found (alt) | Same as 3702 | +| `3711` | CANT_HOST_CONCURRENT | Can't host multiple meetings | End other meeting first | + +### Platform Restrictions + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3603` | NOT_SUPPORT_WEBCLIENT | Web join not allowed | Admin must enable web client | +| `3608` | TSP_NOT_SUPPORT | TSP audio not supported on web | Use computer audio or phone | +| `3611` | USE_DESKTOP_OR_MOBILE | Browser join disabled | Use Zoom desktop/mobile app | +| `3620` | EMAIL_BLOCKED | Email blocked by admin | Contact account administrator | +| `3621` | NO_RESPONSE_FROM_WEB | Server timeout | Retry request | + +## Connection Errors (4000-4999) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `4000` | RE_CONNECTING | Reconnecting to meeting | Wait for reconnection | +| `4001` | DISCONNECT | Disconnected from meeting | Check network, try rejoining | +| `4003` | INVALID_PARAMETER | Invalid join parameter | Check all required fields | +| `4004` | MEETING_ENDED | Meeting has ended | Cannot join ended meeting | +| `4005` | MEETING_CAPACITY_REACHED | Meeting is full | Host needs to increase capacity | +| `4006` | MEETING_LOCKED | Meeting is locked | Host must unlock to allow joins | +| `4007` | REJECT_BARRIERS | Information barriers rejection | Contact admin about policies | +| `4008` | PARTICIPANT_EXIST | Already a participant | Already in meeting or leave first | +| `4009` | SERVER_ERROR | Internal server error | Retry request | +| `4011` | NOT_ALLOW_CROSS_JOIN | Cross-account join blocked | Publish app on Marketplace (see 6.1 ToS) | + +## OBF/Anonymous Join Errors (March 2026+) + +> **IMPORTANT**: Starting **March 2, 2026**, anonymous joins to external meetings are blocked. You must provide a valid OBF or ZAK token. + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `4012` | NOT_ALLOW_ANONYMOUS_JOIN | Anonymous join not allowed | Provide valid OBF or ZAK token | +| `4013` | USER_LEVEL_TOKEN_NOT_HAVE_HOST_ZAK_OBF | OBF/ZAK token invalid or missing | Verify token is not expired or malformed | + +### 4012 Error Response + +```json +{ + "meetingStatus": 3, + "errorCode": 4012, + "errorMessage": "Anonymous joins are not allowed for this SDK app. Authenticate the Zoom user and provide a ZAK or OBF token." +} +``` + +**Solutions for 4012:** +1. Generate OBF token via Zoom API +2. Or get user's ZAK token via `/users/me/zak` +3. Pass token in `obfToken` or `zak` join parameter + +### 4013 Error Response + +```json +{ + "meetingStatus": 3, + "errorCode": 4013, + "errorMessage": "The OBF or ZAK token is not provided or invalid. Make sure it's not expired or malformed." +} +``` + +**Solutions for 4013:** +1. Check token expiration (OBF tokens expire) +2. Verify token format is correct +3. Regenerate token if expired + +## System Errors (6000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `6603` | BLOCKED_BY_HOST_ADMIN | SDK Key blocked by host's admin | Contact host's admin to whitelist | + +## SDK Version Errors (10000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `10000` | SDK_VERSION_UNSUPPORTED | SDK version no longer supported | Upgrade to latest SDK version | + +## Simulive Errors (13000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `13208` | UNABLE_JOIN_ENDED_SIMULIVE | Simulive webinar has ended | Cannot join ended simulive | + +## Error Handling Patterns + +### Client View + +```javascript +ZoomMtg.join({ + // ... options + success: (res) => { + console.log('Joined successfully'); + }, + error: (err) => { + console.error('Join failed:', err); + + switch (err.errorCode) { + case 3004: + alert('Incorrect meeting password'); + break; + case 3712: + console.error('Signature invalid - check SDK secret'); + break; + case 4012: + console.error('OBF token required for external meetings'); + break; + default: + console.error(`Error ${err.errorCode}: ${err.errorMessage}`); + } + } +}); +``` + +### Component View + +```javascript +try { + await client.join({ + // ... options + }); +} catch (error) { + console.error('Join failed:', error); + + // error.reason contains error code + // error.message contains description + + if (error.reason === 'WRONG_MEETING_PASSWORD') { + alert('Incorrect password'); + } +} +``` + +### Listen for Connection Changes + +```javascript +// Client View +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + if (data.status === 3) { + console.error('Disconnected:', data.errorCode); + } +}); + +// Component View +client.on('connection-change', (payload) => { + if (payload.state === 'Closed') { + console.error('Connection closed:', payload.reason); + } +}); +``` + +## Common Error Scenarios + +### "Signature is invalid" (3712) + +**Causes:** +1. SDK Secret doesn't match SDK Key +2. Signature generated with wrong algorithm (must be HS256) +3. Clock skew between server and Zoom +4. Missing or incorrect `appKey` field in signature + +**Debug Steps:** +1. Verify SDK Key and Secret in Marketplace +2. Check signature generation code +3. Ensure server time is accurate (NTP sync) +4. Test with official [auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) + +### "Meeting does not exist" (3001/3610) + +**Causes:** +1. Typo in meeting number +2. Meeting was deleted +3. Meeting ID vs Meeting Number confusion + +**Note:** Meeting ID (from API) and Meeting Number (shown in client) are the same. + +### "Anonymous join not allowed" (4012) + +**Causes:** +1. Joining meeting outside your account without authorization +2. No OBF or ZAK token provided + +**Solutions:** +1. For bots: Use App Privilege Token (OBF) +2. For users: Get their ZAK token +3. For same-account meetings: No token needed + +### "Cross-account join blocked" (4011) + +**Cause:** Your app isn't published on Marketplace and is trying to join meetings outside your account. + +**Solutions:** +1. Publish your app on Zoom Marketplace +2. Or only join meetings within your account +3. Or use OBF token for authorization + +## OBF Token Timeline + +| Date | Enforcement | +|------|-------------| +| **February 7, 2026** | If OBF provided, must be valid (not expired/malformed) | +| **March 2, 2026** | Cannot join external meetings without valid OBF/ZAK | + +## Additional Resources + +- [OBF FAQ](https://developers.zoom.us/docs/meeting-sdk/obf-faq/) +- [Signature Generation](https://developers.zoom.us/docs/meeting-sdk/auth/) +- [ZAK Token API](https://developers.zoom.us/docs/api/users/#tag/users/get/users/me/zak) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/RUNBOOK.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/RUNBOOK.md new file mode 100644 index 00000000..af265e53 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Windows 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Meeting SDK embed path for Windows (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/windows/ +- https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/windows/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/windows/` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/SKILL.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/SKILL.md new file mode 100644 index 00000000..8303a4bb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/SKILL.md @@ -0,0 +1,1193 @@ +--- +name: zoom-meeting-sdk-windows +description: | + Zoom Meeting SDK for Windows - Native C++ SDK for embedding Zoom meetings into Windows desktop + applications. Supports custom UI architecture with raw video/audio data, headless bots, and deep + integration with meeting features. Includes SDK architecture patterns and Windows message loop handling. +user-invocable: false +triggers: + - "meeting sdk windows" + - "windows meeting sdk" + - "windows meeting bot" + - "c++ meeting sdk windows" + - "custom ui meeting windows" +--- + +# Zoom Meeting SDK (Windows) + +Embed Zoom meeting capabilities into Windows desktop applications for native C++ integrations and headless bots. + +## New to Zoom SDK? Start Here! + +**The fastest way to master the SDK:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Learn the universal pattern that works for ALL 35+ features +2. **[Authentication Pattern](examples/authentication-pattern.md)** - Get a working bot joining meetings +3. **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - Fix the #1 reason callbacks don't fire + +**Building a Custom UI?** +- [Custom UI Architecture](concepts/custom-ui-architecture.md) - How SDK rendering actually works (child HWNDs, D3D, etc.) +- [Custom UI Video Rendering Example](examples/custom-ui-video-rendering.md) - Complete working code +- [SDK-Rendered vs Self-Rendered](concepts/custom-ui-vs-raw-data.md) - Choose the right approach +- [Custom UI Interface Methods](references/interface-methods.md) - All 13 required virtual methods + +**Having issues?** +- Build errors → [Build Errors Guide](troubleshooting/build-errors.md) +- Callbacks not firing → [Windows Message Loop](troubleshooting/windows-message-loop.md) +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) +- Performance / service quality → [service-quality.md](examples/service-quality.md) +- Deployment notes → [deployment.md](references/deployment.md) +- MSBuild from git bash → [Build Errors Guide](troubleshooting/build-errors.md#msbuild-command-pattern) +- Complete navigation → [SKILL.md](SKILL.md) + +## Prerequisites + +- Zoom app with Meeting SDK credentials (Client ID & Secret) +- Visual Studio 2019/2022 or later +- Windows 10 or later +- C++ development environment +- vcpkg for dependency management + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT token generation. + +## Project Preferences & Learnings + +> **IMPORTANT**: These are hard-won preferences from real project experience. Follow these when creating new projects. + +### Do NOT use CMake — Use native Visual Studio `.vcxproj` + +**Always create a native Visual Studio `.sln` + `.vcxproj` project**, not a CMake project. Reasons: +- More standard and familiar for Windows C++ developers +- Developers can double-click the `.sln` to open in Visual Studio immediately +- Project settings (include dirs, lib dirs, preprocessor defines) are easier to see and edit in the VS Property Pages UI +- No extra CMake tooling or configuration step required +- Friendlier and easier for developers to understand and maintain + +### `config.json` must be visible in Solution Explorer + +The `config.json` file (containing `sdk_jwt`, `meeting_number`, `passcode`) must be: +1. **Included in the `.vcxproj`** as a `` item with `PreserveNewest` +2. **Placed in a "Config" filter** in the `.vcxproj.filters` file so it appears under a "Config" folder in Solution Explorer +3. **Easily editable** by developers directly from Solution Explorer — they should never have to hunt for it in File Explorer + +Example `.vcxproj` entry: +```xml + + + PreserveNewest + + +``` + +Example `.vcxproj.filters` entry: +```xml + + + {GUID-HERE} + + + + + Config + + +``` + +## Overview + +The Windows SDK is a **C++ native SDK** designed for: +- **Desktop applications** - Native Windows apps with full UI control +- **Headless bots** - Join meetings without UI +- **Raw media access** - Capture/send audio/video streams +- **Local recording** - Record meetings locally or to cloud + +### Key Architectural Insight + +The SDK follows a **universal 3-step pattern** for every feature: +1. **Get controller** (singleton): `meetingService->Get[Feature]Controller()` +2. **Implement event listener**: `class MyListener : public I[Feature]Event { ... }` +3. **Register and use**: `controller->SetEvent(listener)` then call methods + +**This works for ALL features**: audio, video, chat, recording, participants, screen sharing, breakout rooms, webinars, Q&A, polling, whiteboard, and 20+ more! + +Learn more: **[SDK Architecture Pattern Guide](concepts/sdk-architecture-pattern.md)** + +## Quick Start + +### 1. Download Windows SDK + +Download from [Zoom Marketplace](https://marketplace.zoom.us/): +- Extract `zoom-meeting-sdk-windows_x86_64-{version}.zip` + +### 2. Setup Project Structure + +``` +your-project/ + YourApp/ + SDK/ + x64/ + bin/ # DLL files and dependencies + h/ # Header files + lib/ # sdk.lib + x86/ + bin/ + h/ + lib/ + YourApp.cpp + YourApp.vcxproj + config.json +``` + +Copy SDK files: +```cmd +xcopy /E /I sdk-package\x64 your-project\YourApp\SDK\x64\ +xcopy /E /I sdk-package\x86 your-project\YourApp\SDK\x86\ +``` + +### 3. Install Dependencies (vcpkg) + +```powershell +# Install vcpkg +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install + +# Install dependencies +.\vcpkg install jsoncpp:x64-windows +.\vcpkg install curl:x64-windows +``` + +### 4. Configure Visual Studio Project + +**Project Properties → C/C++ → General → Additional Include Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\h +C:\vcpkg\packages\jsoncpp_x64-windows\include +C:\vcpkg\packages\curl_x64-windows\include +``` + +**Project Properties → Linker → General → Additional Library Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\lib +``` + +**Project Properties → Linker → Input → Additional Dependencies:** +``` +sdk.lib +``` + +**Post-Build Event** (Copy DLLs to output): +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +``` + +### 5. Configure Credentials + +Create `config.json`: +```json +{ + "sdk_jwt": "YOUR_JWT_TOKEN", + "meeting_number": "1234567890", + "passcode": "password123", + "zak": "" +} +``` + +### 6. Build & Run + +- Open solution in Visual Studio +- Select x64 or x86 configuration +- Press F5 to build and run + +## Core Workflow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AuthSDK │───►│ JoinMeeting │───►│ Raw Data │ +│ │ │ (JWT) │ │ │ │ Subscribe │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + ▼ ▼ + OnAuthComplete onInMeeting + callback callback +``` + +**⚠️ CRITICAL**: Add Windows message loop or callbacks won't fire! +```cpp +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` +See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +## Code Examples + +> **💡 Pro Tip**: These are minimal examples. For complete, tested code see: +> - [Authentication Pattern](examples/authentication-pattern.md) - Full auth workflow +> - [Raw Video Capture](examples/raw-video-capture.md) - Complete video capture +> - [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Implement any feature + +### Important: Include Order + +**CRITICAL**: Include headers in this exact order or you'll get build errors: + +```cpp +#include // MUST be first +#include // MUST be second (SDK headers use uint32_t) +// ... other standard headers ... +#include +#include // BEFORE participants! +#include +``` + +See: [Build Errors Guide](troubleshooting/build-errors.md) for all dependency fixes. + +### 1. Initialize SDK + +```cpp +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +bool InitMeetingSDK() { + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + initParam.strSupportUrl = L"https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::wcout << L"InitSDK failed: " << err << std::endl; + return false; + } + return true; +} +``` + +### 2. Authenticate with JWT + +```cpp +#include +#include +#include +#include "AuthServiceEventListener.h" + +IAuthService* authService = nullptr; + +void OnAuthenticationComplete() { + std::cout << "Authentication successful!" << std::endl; + JoinMeeting(); // Proceed to join meeting +} + +bool AuthenticateSDK(const std::wstring& jwtToken) { + CreateAuthService(&authService); + if (!authService) return false; + + // Set event listener BEFORE calling SDKAuth + authService->SetEvent(new AuthServiceEventListener(&OnAuthenticationComplete)); + + // Authenticate with JWT + AuthContext authContext; + authContext.jwt_token = jwtToken.c_str(); + + SDKError err = authService->SDKAuth(authContext); + if (err != SDKERR_SUCCESS) { + std::wcout << L"SDKAuth failed: " << err << std::endl; + return false; + } + + // CRITICAL: Add message loop or callback won't fire! + // See complete example: examples/authentication-pattern.md + + return true; +} +``` + +**AuthServiceEventListener.h:** +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS && onAuthComplete) { + onAuthComplete(); + } else { + std::cout << "Auth failed: " << ret << std::endl; + } + } + + // Must implement ALL pure virtual methods (6 total) + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override {} + void onLogout() override {} + void onZoomIdentityExpired() override {} + void onZoomAuthIdentityExpired() override {} +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override {} +#endif + +private: + void (*onAuthComplete)(); +}; +``` + +**See complete working code**: [Authentication Pattern Guide](examples/authentication-pattern.md) + +### 3. Join Meeting + +```cpp +#include +#include "MeetingServiceEventListener.h" + +IMeetingService* meetingService = nullptr; + +void OnMeetingJoined() { + std::cout << "Joining meeting..." << std::endl; +} + +void OnInMeeting() { + std::cout << "In meeting now!" << std::endl; + // Start raw data capture here +} + +void OnMeetingEnds() { + std::cout << "Meeting ended" << std::endl; +} + +bool JoinMeeting(UINT64 meetingNumber, const std::wstring& password) { + CreateMeetingService(&meetingService); + if (!meetingService) return false; + + // Set event listener + meetingService->SetEvent( + new MeetingServiceEventListener(&OnMeetingJoined, &OnMeetingEnds, &OnInMeeting) + ); + + // Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = meetingNumber; + params.userName = L"Bot User"; + params.psw = password.c_str(); + params.isVideoOff = false; + params.isAudioOff = false; + + SDKError err = meetingService->Join(joinParam); + if (err != SDKERR_SUCCESS) { + std::wcout << L"Join failed: " << err << std::endl; + return false; + } + return true; +} +``` + +**MeetingServiceEventListener.h:** +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() + ) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_CONNECTING) { + if (onMeetingJoined) onMeetingJoined(); + } + else if (status == MEETING_STATUS_INMEETING) { + if (onInMeetingCallback) onInMeetingCallback(); + } + else if (status == MEETING_STATUS_ENDED) { + if (onMeetingEnded) onMeetingEnded(); + } + } + + // Must implement ALL pure virtual methods (9 total) + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool isActive) override {} + void onMeetingTopicChanged(const zchar_t* sTopic) override {} + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override {} + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, unsigned int userId, bool uplink) override {} +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override {} +#endif + +private: + void (*onMeetingJoined)(); + void (*onMeetingEnded)(); + void (*onInMeetingCallback)(); +}; +``` + +**See all required methods**: [Interface Methods Guide](references/interface-methods.md) + +### 4. Subscribe to Raw Video + +```cpp +#include +#include +#include +#include +#include // REQUIRED for YUVRawDataI420 +#include "ZoomSDKRendererDelegate.h" + +IZoomSDKRenderer* videoHelper = nullptr; +ZoomSDKRendererDelegate* videoSource = new ZoomSDKRendererDelegate(); + +bool StartVideoCapture(uint32_t userId) { + // STEP 1: Start raw recording FIRST (required!) + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + SDKError canStart = recordCtrl->CanStartRawRecording(); + if (canStart != SDKERR_SUCCESS) { + std::cout << "Cannot start recording: " << canStart << std::endl; + return false; + } + + recordCtrl->StartRawRecording(); + + // Wait for recording to initialize + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // STEP 2: Create renderer + SDKError err = createRenderer(&videoHelper, videoSource); + if (err != SDKERR_SUCCESS || !videoHelper) { + std::cout << "createRenderer failed: " << err << std::endl; + return false; + } + + // STEP 3: Set resolution and subscribe + videoHelper->setRawDataResolution(ZoomSDKResolution_720P); + err = videoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO); + if (err != SDKERR_SUCCESS) { + std::cout << "Subscribe failed: " << err << std::endl; + return false; + } + + std::cout << "Video capture started! Frames arrive in onRawDataFrameReceived()" << std::endl; + return true; +} +``` + +**ZoomSDKRendererDelegate.h:** +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + // YUV420 (I420) format: Y plane + U plane + V plane + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Calculate buffer sizes + // Y = full resolution, U/V = quarter resolution each + size_t ySize = width * height; + size_t uvSize = ySize / 4; // (width/2) * (height/2) + + // Total size: width * height * 1.5 bytes + + // Save to file (playback: ffplay -f rawvideo -pixel_format yuv420p -video_size 1280x720 output.yuv) + std::ofstream outputFile("output.yuv", std::ios::binary | std::ios::app); + outputFile.write(data->GetYBuffer(), ySize); // Brightness + outputFile.write(data->GetUBuffer(), uvSize); // Blue-difference + outputFile.write(data->GetVBuffer(), uvSize); // Red-difference + outputFile.close(); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + std::cout << "Raw data status: " << status << std::endl; + } + + void onRendererBeDestroyed() override { + std::cout << "Renderer destroyed" << std::endl; + } +}; +``` + +**Complete video capture guide**: [Raw Video Capture Guide](examples/raw-video-capture.md) + +### 5. Subscribe to Raw Audio + +```cpp +#include + +class ZoomSDKAudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Process PCM audio (mixed from all participants) + std::ofstream pcmFile("audio.pcm", std::ios::binary | std::ios::app); + pcmFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + pcmFile.close(); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Process audio from specific participant + } +}; + +// Subscribe to audio +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +audioHelper->subscribe(new ZoomSDKAudioRawDataDelegate()); +``` + +### 6. Main Message Loop (CRITICAL!) + +**⚠️ WITHOUT THIS, CALLBACKS WON'T FIRE!** + +```cpp +#include +#include +#include + +int main() { + // Initialize COM + CoInitialize(NULL); + + // Load config and initialize + LoadConfig(); + InitMeetingSDK(); + AuthenticateSDK(sdk_jwt); + + // CRITICAL: Windows message loop for SDK callbacks + // SDK uses Windows message pump to dispatch callbacks + // Without this, callbacks are queued but NEVER fire! + MSG msg; + while (!g_exit) { + // Process all pending Windows messages + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Cleanup + CleanSDK(); + CoUninitialize(); + + return 0; +} +``` + +**Why message loop is critical**: The SDK uses Windows COM/messaging for async callbacks. Without `PeekMessage()`, the SDK queues messages but they're never retrieved/dispatched, so callbacks never execute. + +**Symptoms without message loop**: +- Authentication timeout (even with valid JWT) +- Meeting join timeout +- No callback events fire +- Appears like network/auth issue but it's a message loop issue + +**See detailed explanation**: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +## Common Issues & Solutions + +| Issue | Solution | +|-------|----------| +| **Callbacks don't fire / Auth timeout** | Add Windows message loop → [Guide](troubleshooting/windows-message-loop.md) | +| **`uint32_t` / `AudioType` errors** | Fix include order → [Guide](troubleshooting/build-errors.md) | +| **Abstract class error** | Implement all virtual methods → [Guide](references/interface-methods.md) | +| **How to implement [feature]?** | Follow universal pattern → [Guide](concepts/sdk-architecture-pattern.md) | +| **Authentication fails** | Check JWT token & error codes → [Guide](troubleshooting/common-issues.md) | +| **No video frames received** | Call StartRawRecording() first → [Guide](examples/raw-video-capture.md) | + +**Complete troubleshooting**: [Common Issues Guide](troubleshooting/common-issues.md) + +## How to Implement Any Feature + +The SDK has **35+ feature controllers** (audio, video, chat, recording, participants, screen sharing, breakout rooms, webinars, Q&A, polling, whiteboard, captions, AI companion, etc.). + +**Universal pattern that works for ALL features:** + +1. **Get the controller** (singleton): + ```cpp + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + // ... 33 more controllers available + ``` + +2. **Implement event listener** (observer pattern): + ```cpp + class MyAudioListener : public IMeetingAudioCtrlEvent { + void onUserAudioStatusChange(IList* lst) override { + // React to audio events + } + // ... implement all required methods + }; + ``` + +3. **Register and use**: + ```cpp + audioCtrl->SetEvent(new MyAudioListener()); + audioCtrl->MuteAudio(userId, true); // Use feature + ``` + +**Complete guide with examples**: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Available Examples + +| Example | Description | +|---------|-------------| +| **SkeletonDemo** | Minimal join meeting - start here | +| **GetVideoRawData** | Subscribe to raw video streams | +| **GetAudioRawData** | Subscribe to raw audio streams | +| **SendVideoRawData** | Send custom video as virtual camera | +| **SendAudioRawData** | Send custom audio as virtual mic | +| **GetShareRawData** | Capture screen share content | +| **LocalRecording** | Local MP4 recording | +| **ChatDemo** | In-meeting chat functionality | +| **CaptionDemo** | Closed caption/live transcription | +| **BreakoutDemo** | Breakout room management | + +## Detailed References + +### 🎯 Core Concepts (START HERE!) +- **[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** - **Universal pattern for implementing ANY feature** - Understanding this unlocks the entire SDK! + +### 📚 Complete Examples +- **[examples/authentication-pattern.md](examples/authentication-pattern.md)** - Complete working authentication with JWT tokens +- **[examples/raw-video-capture.md](examples/raw-video-capture.md)** - YUV420 video capture with detailed format explanation + +### 🔧 Troubleshooting Guides +- **[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** - **Why callbacks don't fire** (MOST CRITICAL!) +- **[troubleshooting/build-errors.md](troubleshooting/build-errors.md)** - SDK header dependency issues and fixes +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Quick diagnostic workflow and error code tables + +### 📖 References +- **[references/interface-methods.md](references/interface-methods.md)** - How to implement ALL required virtual methods +- **[references/windows-reference.md](references/windows-reference.md)** - Dependencies, Visual Studio setup +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### 🎨 Feature-Specific Guides +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout room management +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-windows-raw-recording-sample](https://github.com/zoom/meetingsdk-windows-raw-recording-sample) | Official raw data capture samples | +| [meetingsdk-windows-local-recording-sample](https://github.com/zoom/meetingsdk-windows-local-recording-container-sample) | Local recording with Docker | + +## Playing Raw Video/Audio Files + +Raw YUV/PCM files have no headers - you must specify format explicitly. + +### Play Raw YUV Video +```cmd +ffplay -video_size 1280x720 -pixel_format yuv420p -f rawvideo output.yuv +``` + +### Convert YUV to MP4 +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv -c:v libx264 output.mp4 +``` + +### Play Raw PCM Audio +```cmd +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert PCM to WAV +```cmd +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### Combine Video + Audio +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv ^ + -f s16le -ar 32000 -ac 1 -i audio.pcm ^ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +**Key flags:** +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (e.g., 1280x720) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate (Zoom uses 32kHz) | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Use one of: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token` in JoinParam) +- **ZAK Token** - Zoom Access Key (`userZAK` in JoinParam) +- **On Behalf Token** - For specific use cases (`onBehalfToken` in JoinParam) + +## 📖 Complete Documentation Library + +This skill includes comprehensive guides created from real-world debugging: + +### 🎯 Start Here +- **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Master document: Universal pattern for ANY feature +- **[SKILL.md](SKILL.md)** - Complete navigation guide + +### 📚 By Category + +**Core Concepts:** +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - How every feature works (singleton + observer pattern) + +**Complete Examples:** +- [Authentication Pattern](examples/authentication-pattern.md) - Working JWT auth with all code +- [Raw Video Capture](examples/raw-video-capture.md) - YUV420 video capture explained + +**Troubleshooting:** +- [Windows Message Loop](troubleshooting/windows-message-loop.md) - **CRITICAL**: Why callbacks don't fire +- [Build Errors](troubleshooting/build-errors.md) - SDK header dependency fixes +- [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics & error codes + +**References:** +- [Interface Methods](references/interface-methods.md) - All required virtual methods (6 auth + 9 meeting) +- [Windows Reference](references/windows-reference.md) - Platform setup +- [Authorization](../references/authorization.md) - JWT generation +- [Bot Authentication](../references/bot-authentication.md) - Bot token types +- [Breakout Rooms](../references/breakout-rooms.md) - Breakout room API +- [AI Companion](../references/ai-companion.md) - AI features + +### 🚨 Most Critical Issues (From Real Debugging) + +1. **Callbacks not firing** → Missing Windows message loop (99% of issues) + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Build errors** → SDK header dependencies (`uint32_t`, `AudioType`, etc.) + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +3. **Abstract class errors** → Missing virtual method implementations + - See: [Interface Methods Guide](references/interface-methods.md) + +### 💡 Key Insight + +**Once you learn the 3-step pattern, you can implement ANY of the 35+ features:** +1. Get controller → 2. Implement event listener → 3. Register and use + +See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Official Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html +- **Developer forum**: https://devforum.zoom.us/ +- **SDK download**: https://marketplace.zoom.us/ + +--- + +**Documentation Version**: Based on Zoom Windows Meeting SDK v6.7.2.26830 + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from meeting-sdk/windows/SKILL.md + +# Zoom Windows Meeting SDK - Complete Documentation Index + +## 🚀 Quick Start Path + +**If you're new to the SDK, follow this order:** + +1. **Read the architecture pattern** → [concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md) + - This teaches you the universal formula that applies to ALL features + - Once you understand this, you can implement any feature by reading the `.h` files + +2. **Fix build errors** → [troubleshooting/build-errors.md](troubleshooting/build-errors.md) + - SDK header dependencies issues + - Required include order + +3. **Implement authentication** → [examples/authentication-pattern.md](examples/authentication-pattern.md) + - Complete working JWT authentication code + +4. **Fix callback issues** → [troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md) + - **CRITICAL**: Why callbacks don't fire without Windows message loop + - This was the hardest issue to diagnose! + +5. **Implement virtual methods** → [references/interface-methods.md](references/interface-methods.md) + - Complete lists of all required methods + - How to avoid abstract class errors + +6. **Capture video (optional)** → [examples/raw-video-capture.md](examples/raw-video-capture.md) + - YUV420 format explained + - Complete raw data capture workflow + +7. **Troubleshoot any issues** → [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Quick diagnostic checklist + - Error code tables + - "If you see X, do Y" reference + +--- + +## 📂 Documentation Structure + +``` +meeting-sdk/windows/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── sdk-architecture-pattern.md # THE MOST IMPORTANT DOC +│ │ # Universal formula for ANY feature +│ ├── singleton-hierarchy.md # Navigation guide for SDK services +│ │ # 4-level deep service tree, when/how +│ ├── custom-ui-architecture.md # How Custom UI rendering works +│ │ # Child HWNDs, D3D, layout, events +│ └── custom-ui-vs-raw-data.md # SDK-rendered vs self-rendered +│ # Decision guide for Custom UI approach +│ +├── examples/ # Complete working code +│ ├── authentication-pattern.md # JWT auth with full code +│ ├── raw-video-capture.md # Video capture with YUV420 details +│ │ # Recording vs Streaming, permissions +│ ├── custom-ui-video-rendering.md # Custom UI with video container +│ │ # Active speaker + gallery layout +│ ├── breakout-rooms.md # Complete breakout room guide +│ │ # 5 roles, create/manage/join +│ ├── chat.md # Send/receive chat messages +│ │ # Rich text, threading, file transfer +│ ├── captions-transcription.md # Live transcription & closed captions +│ │ # Multi-language translation +│ ├── local-recording.md # Local MP4 recording +│ │ # Permission flow, encoder monitoring +│ ├── share-raw-data-capture.md # Screen share raw data capture +│ │ # YUV420 frames from shared content +│ └── send-raw-data.md # Virtual camera/mic/share +│ # Send custom video/audio/share +│ +├── troubleshooting/ # Problem solving guides +│ ├── windows-message-loop.md # CRITICAL - Why callbacks fail +│ ├── build-errors.md # Header dependency fixes + MSBuild +│ └── common-issues.md # Quick diagnostic workflow +│ +└── references/ # Reference documentation + ├── interface-methods.md # Required virtual methods + │ # Auth(6) + Meeting(9) + CustomUI(13) + ├── windows-reference.md # Platform setup + ├── authorization.md # JWT generation + ├── bot-authentication.md # Bot token types + ├── breakout-rooms.md # Breakout room features + └── ai-companion.md # AI Companion features +``` + +--- + +## 🎯 By Use Case + +### I want to build a meeting bot +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Understand the pattern +2. [Authentication Pattern](examples/authentication-pattern.md) - Join meetings +3. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Fix callback issues +4. [Interface Methods](references/interface-methods.md) - Implement callbacks + +### I'm getting build errors +1. [Build Errors Guide](troubleshooting/build-errors.md) - SDK header dependencies +2. [Interface Methods](references/interface-methods.md) - Abstract class errors +3. [Common Issues](troubleshooting/common-issues.md) - Linker errors + +### I'm getting runtime errors +1. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callbacks not firing +2. [Authentication Pattern](examples/authentication-pattern.md) - Auth timeout +3. [Common Issues](troubleshooting/common-issues.md) - Error code tables + +### I want to build a Custom UI meeting app +1. [Custom UI Architecture](concepts/custom-ui-architecture.md) - How SDK rendering works +2. [SDK-Rendered vs Self-Rendered](concepts/custom-ui-vs-raw-data.md) - Choose your approach +3. [Custom UI Video Rendering](examples/custom-ui-video-rendering.md) - Complete working code +4. [Interface Methods](references/interface-methods.md) - 13 Custom UI virtual methods +5. [Build Errors Guide](troubleshooting/build-errors.md) - MSBuild from git bash + +### I want to capture video/audio +1. [Raw Video Capture](examples/raw-video-capture.md) - Complete video workflow + - Recording vs Streaming approaches + - Permission requirements (host, OAuth tokens) + - Audio PCM capture +2. [Share Raw Data Capture](examples/share-raw-data-capture.md) - Screen share capture + - Subscribe to RAW_DATA_TYPE_SHARE + - Handle dynamic resolution +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Controller pattern +4. [Common Issues](troubleshooting/common-issues.md) - No frames received + +### I want to use breakout rooms +1. [Breakout Rooms Guide](examples/breakout-rooms.md) - Complete breakout room workflow + - 5 roles: Creator, Admin, Data, Assistant, Attendee + - Create, configure, manage, join/leave rooms +2. [Common Issues](troubleshooting/common-issues.md) - Breakout room error codes + +### I want to implement chat +1. [Chat Guide](examples/chat.md) - Send/receive messages + - Rich text formatting (bold, italic, links) + - Private messages and threading + - File transfer events + +### I want to use live transcription +1. [Captions & Transcription Guide](examples/captions-transcription.md) - Live transcription + - Automatic speech-to-text + - Multi-language translation + - Manual closed captions (host feature) + +### I want to record meetings +1. [Local Recording Guide](examples/local-recording.md) - Local MP4 recording + - Permission request workflow + - zTscoder.exe encoder monitoring + - Gallery view vs active speaker + +### I want to implement a specific feature +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - **START HERE!** +2. Find the controller in `SDK/x64/h/meeting_service_interface.h` +3. Find the header in `SDK/x64/h/meeting_service_components/` +4. Follow the universal pattern: Get controller → Implement listener → Use methods + +### I want to understand the SDK architecture +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Complete architecture overview +2. [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigate the service tree (4 levels) +3. [Interface Methods](references/interface-methods.md) - Event listener pattern +4. [Authentication Pattern](examples/authentication-pattern.md) - Service pattern + +--- + +## 🔥 Most Critical Documents + +### 1. SDK Architecture Pattern (⭐ MASTER DOCUMENT) +**[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** + +This is THE most important document. It teaches the universal 3-step pattern: +1. Get controller (singleton pattern) +2. Implement event listener (observer pattern) +3. Register and use + +Once you understand this pattern, you can implement **any of the 35+ features** by just reading the SDK headers. + +**Key insight**: The Zoom SDK follows a perfectly consistent architecture. Every feature works the same way. + +--- + +### 2. Windows Message Loop (⚠️ MOST COMMON ISSUE) +**[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** + +99% of "callbacks not firing" issues are caused by missing Windows message loop. This document explains: +- Why SDK requires `PeekMessage()` loop +- How to implement it correctly +- How to diagnose callback issues + +**This was the hardest bug to find during development** (took ~2 hours). + +--- + +### 3. Build Errors Guide +**[troubleshooting/build-errors.md](troubleshooting/build-errors.md)** + +SDK headers have dependency bugs that cause build errors. This document provides: +- Required include order +- Missing `` fix +- Missing `AudioType` fix +- Missing `YUVRawDataI420` fix + +--- + +## 📊 By Document Type + +### Concepts (Why and How) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Universal implementation pattern +- [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigation guide for SDK services (4 levels deep) + +### Examples (Complete Working Code) +- [Authentication Pattern](examples/authentication-pattern.md) - JWT authentication +- [Raw Video Capture](examples/raw-video-capture.md) - Video capture with YUV420, recording vs streaming +- [Custom UI Video Rendering](examples/custom-ui-video-rendering.md) - SDK-rendered video containers +- [Breakout Rooms](examples/breakout-rooms.md) - Create, manage, join breakout rooms +- [Chat](examples/chat.md) - Send/receive messages with rich formatting +- [Captions & Transcription](examples/captions-transcription.md) - Live transcription and closed captions +- [Local Recording](examples/local-recording.md) - Local MP4 recording with permission flow +- [Share Raw Data Capture](examples/share-raw-data-capture.md) - Screen share raw data capture +- [Send Raw Data](examples/send-raw-data.md) - Virtual camera, microphone, and share + +### Troubleshooting (Problem Solving) +- [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callback issues +- [Build Errors](troubleshooting/build-errors.md) - Compilation issues +- [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics + +### References (Lookup Information) +- [Interface Methods](references/interface-methods.md) - Required virtual methods +- [Windows Reference](references/windows-reference.md) - Platform setup +- [Authorization](../references/authorization.md) - JWT generation +- [Bot Authentication](../references/bot-authentication.md) - Bot tokens +- [Breakout Rooms](../references/breakout-rooms.md) - Breakout room API +- [AI Companion](../references/ai-companion.md) - AI features + +--- + +## 💡 Key Learnings from Real Debugging + +These documents were created from actual debugging of a non-functional Zoom SDK sample. Here are the key insights: + +### Critical Discoveries: + +1. **Windows Message Loop is MANDATORY** (not optional) + - SDK uses Windows message pump for callbacks + - Without it, callbacks are queued but never fire + - Manifests as "authentication timeout" even with valid JWT + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **SDK Headers Have Dependency Bugs** + - Missing `#include ` in SDK headers + - `meeting_participants_ctrl_interface.h` doesn't include `meeting_audio_interface.h` + - `rawdata_renderer_interface.h` only forward-declares `YUVRawDataI420` + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +3. **Include Order is CRITICAL** + - `` must be FIRST + - `` must be SECOND + - Then SDK headers in specific order + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +4. **ALL Virtual Methods Must Be Implemented** + - Including WIN32-conditional methods + - SDK v6.7.2 requires 6 auth methods + 9 meeting methods + - Different versions have different requirements + - See: [Interface Methods Guide](references/interface-methods.md) + +5. **The Architecture is Beautifully Consistent** + - Every feature follows the same 3-step pattern + - Controllers are singletons + - Event listeners use observer pattern + - Once you learn the pattern, you can implement any feature + - See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +--- + +## 🎓 Learning Path by Skill Level + +### Beginner (Never used Zoom SDK) +1. Read [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) to understand the overall design +2. Follow [Authentication Pattern](examples/authentication-pattern.md) to join your first meeting +3. Reference [Common Issues](troubleshooting/common-issues.md) when you hit problems + +### Intermediate (Familiar with SDK basics) +1. Deep dive into [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - implement multiple features +2. Learn [Raw Video Capture](examples/raw-video-capture.md) for media processing +3. Use [Interface Methods](references/interface-methods.md) as reference + +### Advanced (Building production bots) +1. Study [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - learn to implement ANY feature +2. Master [Windows Message Loop](troubleshooting/windows-message-loop.md) - understand async callback flow +3. Reference SDK headers directly using the universal pattern + +--- + +## 🔍 How to Find What You Need + +### "My code won't compile" +→ [Build Errors Guide](troubleshooting/build-errors.md) + +### "Authentication times out" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Callbacks never fire" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Abstract class error" +→ [Interface Methods](references/interface-methods.md) + +### "How do I implement [feature]?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I join a meeting?" +→ [Authentication Pattern](examples/authentication-pattern.md) + +### "How do I capture video?" +→ [Raw Video Capture](examples/raw-video-capture.md) + +### "What error code means what?" +→ [Common Issues](troubleshooting/common-issues.md) - Comprehensive error code tables (SDKERR, AUTHRET, Login, BO, Phone, OBF) + +### "How do I use breakout rooms?" +→ [Breakout Rooms Guide](examples/breakout-rooms.md) + +### "How does the SDK work?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I navigate to a specific controller/feature?" +→ [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +### "How do I send/receive chat messages?" +→ [Chat Guide](examples/chat.md) + +### "How do I use live transcription?" +→ [Captions & Transcription Guide](examples/captions-transcription.md) + +### "How do I record locally?" +→ [Local Recording Guide](examples/local-recording.md) + +### "How do I capture screen share?" +→ [Share Raw Data Capture](examples/share-raw-data-capture.md) + +--- + +## 📝 Document Version + +All documents are based on **Zoom Windows Meeting SDK v6.7.2.26830**. + +Different SDK versions may have: +- Different required callback methods +- Different error codes +- Different API behavior + +If using a different version, use `grep "= 0" SDK/x64/h/*.h` to verify required methods. + +--- + +Remember: The [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) is the fastest way to understand how the Windows Meeting SDK fits together. Read it first if you are debugging custom UI or event flow issues. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md new file mode 100644 index 00000000..50c58e95 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md @@ -0,0 +1,224 @@ +# Custom UI Architecture — How It Actually Works + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Concepts +> **Prerequisite**: [SDK Architecture Pattern](sdk-architecture-pattern.md) + +## Overview + +Custom UI mode lets you create your OWN meeting window instead of the SDK's default meeting UI. The SDK renders video into your window using Direct3D, but you control all layout, window management, and UI elements. + +**This is NOT "HWND hijacking."** The SDK creates child windows inside your parent window and renders into those using its own D3D pipeline. Your window and WndProc remain untouched. + +## Enabling Custom UI Mode + +Set `ENABLE_CUSTOMIZED_UI_FLAG` during SDK initialization: + +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +initParam.emLanguageID = LANGUAGE_English; + +// CRITICAL: Enable Custom UI mode +initParam.obConfigOpts.optionalFeatures = ENABLE_CUSTOMIZED_UI_FLAG; + +SDKError err = InitSDK(initParam); +``` + +Without this flag, the SDK creates its own default meeting window. With it, the SDK creates NO UI — you must provide everything. + +## Internal Architecture + +``` +Your Window (WS_OVERLAPPEDWINDOW) — you own this, your WndProc + | + +-- [SDK Child HWND] — created internally by CreateVideoContainer() + | - SDK owns the WndProc + | - D3D11 swap chain bound to this child HWND + | - Composites all video onto one surface + | + +-- VideoElement: Active (logical RECT region, NOT a window) + +-- VideoElement: Normal0 (logical RECT region, NOT a window) + +-- VideoElement: Normal1 (logical RECT region, NOT a window) + +-- ... + | + +-- [SDK Child HWND for Share] — created by CreateShareRender() + | - Separate D3D surface for screen share content + | - Requires HandleWindowsMoveMsg() for DWM resync +``` + +### Key architectural facts + +1. **`CreateVideoContainer(hParentWnd, rc)`** — SDK creates a **child HWND** inside your parent. You never see or manage this child HWND directly. + +2. **Video elements are NOT separate windows** — they are **logical render regions** within a single D3D surface. `SetPos(RECT)` tells the SDK's compositor where to place each video texture within the container. + +3. **Your app does ZERO rendering** — no `WM_PAINT`, no GDI calls, no `BitBlt`. The SDK handles 100% of video drawing internally. + +## Rendering Technology + +The SDK supports multiple rendering backends, configurable via `InitParam.renderOpts.videoRenderMode`: + +```cpp +enum ZoomSDKVideoRenderMode { + ZoomSDKVideoRenderMode_None = 0, // Auto (default) + ZoomSDKVideoRenderMode_Auto, + ZoomSDKVideoRenderMode_D3D11EnableFLIP, // D3D11 with DXGI flip model (best) + ZoomSDKVideoRenderMode_D3D11, // D3D11 standard + ZoomSDKVideoRenderMode_D3D9, // D3D9 fallback + ZoomSDKVideoRenderMode_GDI, // GDI software fallback (VMs) +}; +``` + +**Hierarchy**: D3D11 FLIP > D3D11 > D3D9 > GDI + +The D3D11 FLIP model uses `DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL` — this requires a dedicated child HWND (further confirming the child window architecture). + +## Why onWindowMsgNotification Exists + +The SDK's child HWND has its own WndProc that **intercepts input messages**. Your parent window's WndProc never sees mouse/keyboard events that land on the video area. The SDK forwards them back to you through callbacks: + +``` +Forwarded messages: + WM_MOUSEMOVE, WM_MOUSEENTER, WM_MOUSELEAVE, + WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONUP, + WM_LBUTTONDBLCLK, WM_KEYDOWN +``` + +If you need to handle clicks on the video (e.g., click a participant to select them), you must handle them in `onWindowMsgNotification`, not in your parent WndProc. + +## Why HandleWindowsMoveMsg() Exists (Share Render Only) + +```cpp +// On ICustomizedShareRender only +virtual SDKError HandleWindowsMoveMsg() = 0; +``` + +When using Direct3D, the swap chain's presentation is tied to the window's screen position via DWM (Desktop Window Manager) composition. When the parent window moves: + +1. Child HWND moves with it automatically +2. BUT the D3D swap chain may not update its DWM surface coordinates immediately +3. This causes "ghost frame" / "stale composition" artifacts + +`HandleWindowsMoveMsg()` tells the SDK to force re-present at the new coordinates. + +**This only exists on `ICustomizedShareRender`**, not on `ICustomizedVideoContainer` — the video container likely handles this internally or uses the FLIP model which doesn't have this issue. + +## HasLicense() Check + +The `ICustomizedUIMgr` interface has a `HasLicense()` method. The official SDK demo checks it as a hard gate: + +```cpp +SDKError err = m_pCustomUIMgr->HasLicense(); +if (err != SDKERR_SUCCESS) { + // Demo aborts here +} +``` + +In practice, modern SDK licenses may include Custom UI by default. It's safe to log a warning but continue if it fails — the SDK will return errors on actual API calls if the license is truly missing. + +## Custom UI Object Lifecycle + +### Creation order (during meeting connect): +1. `CreateCustomizedUIMgr(&pMgr)` — global, creates the manager +2. `pMgr->SetEvent(&listener)` — register for destroy notifications +3. `pMgr->CreateVideoContainer(&pContainer, hParentWnd, rc)` — creates the rendering surface +4. `pContainer->SetEvent(&containerListener)` — register for layout/render events +5. `pContainer->Show()` / `SetBkColor()` — configure appearance +6. `pContainer->CreateVideoElement(&pElement, type)` — create render slots + +### Destruction order (when meeting ends): +1. `pContainer->DestroyAllVideoElement()` — remove all render slots +2. `pMgr->DestroyVideoContainer(pContainer)` — destroy the rendering surface +3. `pMgr->DestroyShareRender(pShareRender)` — destroy share render if created +4. `DestroyCustomizedUIMgr(pMgr)` — global cleanup + +### Important: The SDK may also destroy containers on its own (e.g., meeting ends). That's why `ICustomizedUIMgrEvent` has `onVideoContainerDestroyed` and `onShareRenderDestroyed` callbacks — so you can null out your pointers. + +## Video Element Types + +### Active Speaker Element (`VideoRenderElement_ACTIVE`) +- Auto-follows whoever is currently speaking +- No need to subscribe to a specific user +- Call `Start()` to begin, `Stop()` to pause + +```cpp +IVideoRenderElement* pElement = nullptr; +pContainer->CreateVideoElement(&pElement, VideoRenderElement_ACTIVE); +IActiveVideoRenderElement* pActive = dynamic_cast(pElement); +pActive->SetPos(rect); +pActive->Show(); +pActive->Start(); +``` + +### Normal Element (`VideoRenderElement_NORMAL`) +- Shows a specific participant's video +- Must call `Subscribe(userId)` to bind it to a user +- Set resolution with `SetResolution()` + +```cpp +IVideoRenderElement* pElement = nullptr; +pContainer->CreateVideoElement(&pElement, VideoRenderElement_NORMAL); +INormalVideoRenderElement* pNormal = dynamic_cast(pElement); +pNormal->Subscribe(userId); +pNormal->SetResolution(VideoRenderResolution_360p); +pNormal->SetPos(rect); +pNormal->Show(); +``` + +### Preview Element (`VideoRenderElement_PREVIEW`) +- Shows the local camera preview before joining +- Used for pre-meeting camera setup + +## Layout Management + +Video element positions are **RECTs relative to the container's client area**, not screen coordinates. + +When the container receives `onLayoutNotification(RECT wnd_client_rect)`, you should recalculate and re-apply all element positions: + +```cpp +void OnLayoutNotification(RECT clientRect) { + int width = clientRect.right - clientRect.left; + int height = clientRect.bottom - clientRect.top; + + // Active speaker: top 70% + RECT activeRect = { 0, 0, width, (int)(height * 0.7) }; + pActiveElement->SetPos(activeRect); + + // Gallery: bottom 30%, evenly split horizontally + int galleryTop = (int)(height * 0.7); + int elemWidth = width / galleryCount; + for (int i = 0; i < galleryCount; i++) { + RECT r = { i * elemWidth, galleryTop, (i+1) * elemWidth, height }; + normalElements[i]->SetPos(r); + } +} +``` + +## Share Render + +The share render is a **separate SDK child window** for displaying screen shares: + +```cpp +pMgr->CreateShareRender(&pShareRender, hParentWnd, rc); +pShareRender->SetEvent(&shareListener); +pShareRender->Hide(); // Hidden until someone shares + +// When sharing starts (via onSharingSourceNotification): +pShareRender->SetShareSourceID(shareSourceID); +pShareRender->Show(); +pShareRender->SetViewMode(CSM_FULLFILL); // or CSM_LETTER_BOX +``` + +## Relevant SDK DLLs + +- `zVideoUI.dll`, `zVideoApp.dll`, `zVideoAppFrame.dll` — core video rendering +- `avcodec_zm-59.dll`, `avutil_zm-57.dll`, `swscale_zm-6.dll` — FFmpeg decoders +- `clDNN64.dll`, `mkldnn.dll` — Intel DNN for AI features (background blur, etc.) + +--- + +**See also:** +- [Custom UI Working Code Example](../examples/custom-ui-video-rendering.md) +- [Two Approaches: SDK-Rendered vs Self-Rendered](custom-ui-vs-raw-data.md) +- [Custom UI Interface Methods](../references/interface-methods.md) diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md new file mode 100644 index 00000000..96915419 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md @@ -0,0 +1,180 @@ +# Two Approaches to Custom UI: SDK-Rendered vs Self-Rendered + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Concepts +> **Prerequisite**: [Custom UI Architecture](custom-ui-architecture.md), [Raw Video Capture](../examples/raw-video-capture.md) + +## Overview + +There are two fundamentally different ways to build a custom meeting UI with the Zoom SDK. Both require Custom UI mode (`ENABLE_CUSTOMIZED_UI_FLAG`), but they differ in who renders the video. + +## Approach 1: SDK-Rendered (ICustomizedVideoContainer) + +The SDK renders video into your window using Direct3D. You control layout, the SDK controls pixels. + +### How it works +``` +Your Win32 Window + -> SDK creates child HWND inside it + -> SDK renders video via D3D11 into child HWND + -> You call SetPos(RECT) to position video elements +``` + +### You control +- Window creation, sizing, positioning +- Which participants are visible and where (`SetPos`) +- Active speaker vs gallery layout +- Your own UI controls (buttons, toolbar) +- Background color (`SetBkColor`) + +### SDK controls +- Actual video decoding and rendering +- D3D pipeline +- Frame timing +- Video quality / resolution scaling + +### Key APIs +- `CreateCustomizedUIMgr()` / `DestroyCustomizedUIMgr()` +- `ICustomizedUIMgr::CreateVideoContainer()` / `DestroyVideoContainer()` +- `ICustomizedVideoContainer::CreateVideoElement()` +- `IActiveVideoRenderElement::Start()` / `Stop()` +- `INormalVideoRenderElement::Subscribe(userId)` +- `IVideoRenderElement::SetPos(RECT)` / `Show()` / `Hide()` + +### Best for +- Standard meeting applications +- Apps that need a meeting window with video +- Rapid development (less code) +- When you don't need pixel-level access + +### Limitations +- No access to raw video frames +- Cannot apply custom filters/effects +- Cannot render video in your own graphics engine +- Cannot pop individual videos into separate windows (elements are regions within one container) +- Single rendering surface per container + +--- + +## Approach 2: Self-Rendered (IZoomSDKRenderer + Raw Data) + +You get raw YUV420 frames per participant and render them yourself. Maximum control. + +### How it works +``` +SDK decodes video internally + -> IZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420*) + -> You get raw pixel data (Y, U, V planes) + -> You render with your own engine (D3D11, OpenGL, GDI, etc.) +``` + +### You control +- Everything from Approach 1, PLUS: +- Raw pixel access (YUV420 frames) +- Your own rendering pipeline (D3D11, OpenGL, Vulkan, GDI, etc.) +- Custom video processing (filters, watermarks, overlays, effects) +- Multiple separate windows per participant +- Picture-in-picture +- Recording/streaming with effects applied +- Any layout imaginable + +### SDK provides +- Decoded video frames as `YUVRawDataI420*` +- Frame metadata (width, height, rotation) +- Per-participant subscription + +### Key APIs +- `createRenderer()` — creates a renderer for one participant +- `IZoomSDKRenderer::setRawDataResolution()` — set quality +- `IZoomSDKRenderer::subscribe(userId)` — bind to participant +- `IZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420*)` — frame callback +- `IZoomSDKRendererDelegate::onRawDataStatusChanged(RawDataStatus)` — status changes +- Raw recording: `IMeetingRecordingController::StartRawRecording()` + +### Best for +- Custom rendering engines +- Video processing / effects +- Multi-window layouts +- Recording with overlays +- Streaming applications +- Research / computer vision + +### Limitations +- More code to write (you handle all rendering) +- Must convert YUV420 to RGB/BGRA for display +- Must manage frame buffers and timing +- Higher CPU usage if not GPU-accelerated +- Must call `StartRawRecording()` before frames flow + +--- + +## Approach 3: Hybrid (Both Together) + +You can combine both approaches in the same application: + +``` +Custom UI mode enabled + | + +-- ICustomizedVideoContainer for main meeting view + | (SDK renders, you layout) + | + +-- IZoomSDKRenderer for specific participants + (raw frames for recording, processing, pop-out windows) +``` + +### Use cases +- Main meeting window uses SDK rendering (efficient, easy) +- Simultaneously capture raw frames for recording with watermarks +- Pop out a specific participant into a custom-rendered window +- Apply computer vision to one participant's feed while showing others normally + +### How to combine +1. Initialize with `ENABLE_CUSTOMIZED_UI_FLAG` +2. Create `ICustomizedVideoContainer` for the main view +3. Also call `createRenderer()` + `subscribe()` for raw data on specific users +4. Both can run simultaneously + +--- + +## Comparison Table + +| Feature | SDK-Rendered | Self-Rendered | Hybrid | +|---------|-------------|---------------|--------| +| Raw pixel access | No | Yes | Yes (selected users) | +| Custom filters/effects | No | Yes | Yes (selected users) | +| Multiple windows | No (regions in 1 container) | Yes | Yes | +| Rendering effort | Minimal | High | Medium | +| CPU usage | Low (SDK uses D3D) | Higher (unless GPU) | Medium | +| Code complexity | Low | High | Medium | +| Layout flexibility | RECT regions | Unlimited | Mix | +| Active speaker tracking | Built-in (`VideoRenderElement_ACTIVE`) | Manual | Mix | +| Screen share display | Built-in (`ICustomizedShareRender`) | Manual from raw data | Mix | +| Time to implement | Hours | Days | Hours + targeted raw data | + +--- + +## Decision Guide + +**Use SDK-Rendered when:** +- You just need a meeting window with custom layout +- You want to add your own toolbar/controls around the video +- Development speed matters +- You don't need to touch the video pixels + +**Use Self-Rendered when:** +- You need to apply effects/filters to video +- You're building a custom rendering engine +- You need participants in separate windows +- You're doing computer vision / ML on the video +- You need full pixel control + +**Use Hybrid when:** +- You want the easy SDK rendering for the main view +- But also need raw frames for specific purposes (recording, one pop-out window, etc.) + +--- + +**See also:** +- [Custom UI Architecture](custom-ui-architecture.md) — How SDK rendering works internally +- [Custom UI Video Rendering Example](../examples/custom-ui-video-rendering.md) — SDK-rendered approach +- [Raw Video Capture](../examples/raw-video-capture.md) — Self-rendered approach diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..847dc761 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md @@ -0,0 +1,654 @@ +# SDK Architecture Pattern: The Universal Implementation Formula + +## Core Concept + +The Zoom Windows Meeting SDK follows a **consistent architectural pattern** that applies to EVERY feature. Once you understand this pattern, you can implement ANY feature just by reading the `.h` header files. + +--- + +## The Universal Pattern + +Every SDK feature follows this 3-step pattern: + +### 1. Get the Controller/Helper (Singleton Pattern) + +```cpp +// Pattern: meetingService->Get[Feature]Controller() +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +``` + +### 2. Implement the Event Listener Interface (Observer Pattern) + +```cpp +// Pattern: I[Feature]Event or I[Feature]CtrlEvent +class MyAudioListener : public IMeetingAudioCtrlEvent { + void onUserAudioStatusChange(IList* lstAudioStatusChange) override { + // React to audio events + } + // ... implement all pure virtual methods +}; +``` + +### 3. Register Listener and Use Controller Methods + +```cpp +// Pattern: controller->SetEvent(listener), then call methods +audioCtrl->SetEvent(new MyAudioListener()); +audioCtrl->MuteAudio(userId, true); // Use feature +``` + +--- + +## Complete Architecture Overview + +### The Singleton Services + +**Top-level services** (created via global functions): + +```cpp +// Authentication Service (JWT, user login) +IAuthService* authService; +CreateAuthService(&authService); + +// Meeting Service (join, leave, access features) +IMeetingService* meetingService; +CreateMeetingService(&meetingService); +``` + +### Feature Controllers (35+ Available) + +**All controllers** are accessed through `IMeetingService->Get[Feature]Controller()`: + +```cpp +// Audio features +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Video features +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + +// Chat features +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + +// Participants management +IMeetingParticipantsController* participantsCtrl = meetingService->GetMeetingParticipantsController(); + +// Recording features +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); + +// Screen sharing +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); + +// Waiting room +IMeetingWaitingRoomController* waitingRoomCtrl = meetingService->GetMeetingWaitingRoomController(); + +// Webinar features +IMeetingWebinarController* webinarCtrl = meetingService->GetMeetingWebinarController(); + +// Breakout rooms +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); + +// AI Companion +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); + +// Q&A +IMeetingQAController* qaCtrl = meetingService->GetMeetingQAController(); + +// Polling +IMeetingPollingController* pollingCtrl = meetingService->GetMeetingPollingController(); + +// Whiteboard +IMeetingWhiteboardController* whiteboardCtrl = meetingService->GetMeetingWhiteboardController(); + +// Closed captions +IClosedCaptionController* captionCtrl = meetingService->GetMeetingClosedCaptionController(); + +// Live streaming +IMeetingLiveStreamController* liveStreamCtrl = meetingService->GetMeetingLiveStreamController(); + +// Emoji reactions +IEmojiReactionController* emojiCtrl = meetingService->GetMeetingEmojiReactionController(); + +// Interpretation +IMeetingInterpretationController* interpretCtrl = meetingService->GetMeetingInterpretationController(); + +// Remote support +IMeetingRemoteSupportController* remoteSupportCtrl = meetingService->GetMeetingRemoteSupportController(); + +// Encryption +IMeetingEncryptionController* encryptionCtrl = meetingService->GetInMeetingEncryptionController(); + +// ... and 15+ more controllers! +``` + +**Complete list** (35 controllers as of SDK v6.7.2): +1. `GetMeetingAudioController()` - Audio mute/unmute +2. `GetMeetingVideoController()` - Video start/stop +3. `GetMeetingShareController()` - Screen sharing +4. `GetMeetingChatController()` - Meeting chat +5. `GetMeetingParticipantsController()` - Participant management +6. `GetMeetingRecordingController()` - Recording/raw data +7. `GetMeetingWaitingRoomController()` - Waiting room management +8. `GetMeetingWebinarController()` - Webinar features +9. `GetMeetingBOController()` - Breakout rooms +10. `GetMeetingAICompanionController()` - AI features +11. `GetMeetingQAController()` - Q&A management +12. `GetMeetingPollingController()` - Polls/surveys +13. `GetMeetingWhiteboardController()` - Whiteboard +14. `GetMeetingClosedCaptionController()` - Captions +15. `GetMeetingLiveStreamController()` - Live streaming +16. `GetMeetingEmojiReactionController()` - Emoji reactions +17. `GetMeetingInterpretationController()` - Language interpretation +18. `GetMeetingSignInterpretationController()` - Sign language +19. `GetMeetingRemoteSupportController()` - Remote support +20. `GetMeetingEncryptionController()` - E2E encryption +21. `GetMeetingRawArchivingController()` - Raw archiving +22. `GetMeetingReminderController()` - Meeting reminders +23. `GetMeetingSmartSummaryController()` - Smart summary (deprecated) +24. `GetUIController()` - UI controls +25. `GetAnnotationController()` - Annotations +26. `GetMeetingRemoteController()` - Remote control +27. `GetH323Helper()` - H.323 support +28. `GetMeetingPhoneHelper()` - Phone dial-in +29. `GetMeetingRealNameAuthController()` - Real name auth +30. `GetMeetingAANController()` - Auto Accept Notification +31. `GetMeetingImmersiveController()` - Immersive view +32. `GetMeetingDocsController()` - Documents +33. `GetMeetingIndicatorController()` - Meeting indicators +34. `GetMeetingProductionStudioController()` - Production studio +35. And more in future versions... + +--- + +## Deep Dive: Audio Feature Example + +Let's implement audio mute/unmute to demonstrate the pattern: + +### Step 1: Read the Header File + +Open `SDK/x64/h/meeting_service_components/meeting_audio_interface.h`: + +```cpp +// The event listener interface +class IMeetingAudioCtrlEvent { +public: + virtual void onUserAudioStatusChange(IList* lstAudioStatusChange) = 0; + virtual void onUserActiveAudioChange(IList* plstActiveAudioUser) = 0; + // ... more callback methods +}; + +// The controller interface +class IMeetingAudioController { +public: + virtual SDKError SetEvent(IMeetingAudioCtrlEvent* pEvent) = 0; + virtual SDKError JoinVoip() = 0; + virtual SDKError MuteAudio(unsigned int userid, bool allowUnmuteBySelf = true) = 0; + virtual SDKError UnMuteAudio(unsigned int userid) = 0; + // ... more control methods +}; +``` + +### Step 2: Implement the Event Listener + +**AudioEventListener.h**: +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AudioEventListener : public IMeetingAudioCtrlEvent { +public: + // Implement ALL pure virtual methods from IMeetingAudioCtrlEvent + void onUserAudioStatusChange(IList* lstAudioStatusChange) override; + void onUserActiveAudioChange(IList* plstActiveAudioUser) override; + void onHostRequestStartAudio(IRequestStartAudioHandler* handler) override; + void onMuteOnEntryStatusChange(bool bEnabled) override; + // ... implement all other required methods +}; +``` + +**AudioEventListener.cpp**: +```cpp +#include "AudioEventListener.h" + +void AudioEventListener::onUserAudioStatusChange(IList* lstAudioStatusChange) { + if (!lstAudioStatusChange) return; + + int count = lstAudioStatusChange->GetCount(); + for (int i = 0; i < count; i++) { + IUserAudioStatus* audioStatus = lstAudioStatusChange->GetItem(i); + unsigned int userId = audioStatus->GetUserId(); + AudioStatus status = audioStatus->GetStatus(); + + std::cout << "[AUDIO] User " << userId << " status changed: " << status << std::endl; + } +} + +void AudioEventListener::onUserActiveAudioChange(IList* plstActiveAudioUser) { + if (!plstActiveAudioUser) return; + + int count = plstActiveAudioUser->GetCount(); + std::cout << "[AUDIO] Active speakers: " << count << std::endl; +} + +void AudioEventListener::onHostRequestStartAudio(IRequestStartAudioHandler* handler) { + std::cout << "[AUDIO] Host requests unmute" << std::endl; + // Auto-accept or ignore based on your logic + if (handler) { + handler->Accept(); // or handler->Ignore() + } +} + +void AudioEventListener::onMuteOnEntryStatusChange(bool bEnabled) { + std::cout << "[AUDIO] Mute on entry: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} +``` + +### Step 3: Use the Feature + +**main.cpp**: +```cpp +void SetupAudioFeatures() { + // 1. Get the controller (singleton) + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + if (!audioCtrl) { + std::cerr << "Failed to get audio controller" << std::endl; + return; + } + + // 2. Set event listener + audioCtrl->SetEvent(new AudioEventListener()); + + // 3. Use controller methods + + // Join audio (connect to VoIP) + SDKError err = audioCtrl->JoinVoip(); + if (err == SDKERR_SUCCESS) { + std::cout << "[AUDIO] Joined VoIP successfully" << std::endl; + } + + // Mute yourself (userId = 0 or your own user ID) + audioCtrl->MuteAudio(0, true); + std::cout << "[AUDIO] Muted self" << std::endl; + + // Unmute yourself + audioCtrl->UnMuteAudio(0); + std::cout << "[AUDIO] Unmuted self" << std::endl; + + // Mute all participants (host only, userId = 0 for all) + audioCtrl->MuteAudio(0, false); // false = don't allow self-unmute + std::cout << "[AUDIO] Muted all participants" << std::endl; +} +``` + +--- + +## The Pattern Applied to ANY Feature + +### Example: Implementing Chat + +**Step 1: Read header** (`meeting_chat_interface.h`) + +```cpp +class IMeetingChatCtrlEvent { + virtual void onChatMsgNotification(IChatMsgInfo* chatMsg) = 0; + // ... more methods +}; + +class IMeetingChatController { + virtual SDKError SetEvent(IMeetingChatCtrlEvent* pEvent) = 0; + virtual SDKError SendChatTo(const wchar_t* receiver, const wchar_t* content) = 0; + // ... more methods +}; +``` + +**Step 2: Implement listener** + +```cpp +class ChatEventListener : public IMeetingChatCtrlEvent { + void onChatMsgNotification(IChatMsgInfo* chatMsg) override { + std::wcout << L"[CHAT] From: " << chatMsg->GetSenderDisplayName() + << L", Message: " << chatMsg->GetContent() << std::endl; + } +}; +``` + +**Step 3: Use it** + +```cpp +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +chatCtrl->SetEvent(new ChatEventListener()); +chatCtrl->SendChatTo(L"everyone", L"Hello from bot!"); +``` + +### Example: Implementing Screen Share Detection + +**Step 1: Read header** (`meeting_sharing_interface.h`) + +```cpp +class IMeetingShareCtrlEvent { + virtual void onSharingStatus(SharingStatus status, unsigned int userId) = 0; +}; +``` + +**Step 2: Implement listener** + +```cpp +class ShareEventListener : public IMeetingShareCtrlEvent { + void onSharingStatus(SharingStatus status, unsigned int userId) override { + if (status == Sharing_Self_Send_Begin) { + std::cout << "[SHARE] User " << userId << " started sharing" << std::endl; + } else if (status == Sharing_Self_Send_End) { + std::cout << "[SHARE] User " << userId << " stopped sharing" << std::endl; + } + } +}; +``` + +**Step 3: Use it** + +```cpp +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); +shareCtrl->SetEvent(new ShareEventListener()); +``` + +--- + +## Key Architectural Insights + +### 1. Controllers are Singletons + +Each controller is a singleton accessed through `meetingService`: + +```cpp +// Always returns the SAME instance +IMeetingAudioController* ctrl1 = meetingService->GetMeetingAudioController(); +IMeetingAudioController* ctrl2 = meetingService->GetMeetingAudioController(); +// ctrl1 == ctrl2 (same pointer) +``` + +**Why this matters**: You can get the controller anywhere in your code without passing pointers around. + +### 2. Event Listeners Use Observer Pattern + +```cpp +// SDK maintains the listener internally +audioCtrl->SetEvent(new AudioEventListener()); + +// SDK calls listener methods when events occur +// You don't call these methods yourself! +``` + +**Why this matters**: The SDK manages listener lifecycle. Use `new` and let SDK handle cleanup. + +### 3. Controllers Have Both Actions and Queries + +**Actions** (change state): +```cpp +audioCtrl->MuteAudio(userId, true); // DO something +chatCtrl->SendChatTo(L"user", L"hello"); // DO something +``` + +**Queries** (get information): +```cpp +bool isMuted = audioCtrl->IsAudioMuted(userId); // GET information +bool isHost = participantsCtrl->IsHost(userId); // GET information +``` + +### 4. Feature Availability Varies by SDK App Type + +Some features require specific permissions: + +```cpp +// Recording requires special SDK app type +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); +if (recordingCtrl) { + SDKError canRecord = recordingCtrl->CanStartRawRecording(); + if (canRecord == SDKERR_SUCCESS) { + recordingCtrl->StartRawRecording(); + } else { + // Not available (wrong app type, insufficient permissions) + } +} +``` + +--- + +## How to Implement Any Feature (Universal Recipe) + +### Recipe for Success: + +1. **Find the header file**: + - Look in `SDK/x64/h/meeting_service_components/` + - File naming: `meeting_[feature]_interface.h` + - Examples: `meeting_audio_interface.h`, `meeting_chat_interface.h` + +2. **Identify the interfaces**: + - Find `I[Feature]Event` or `I[Feature]CtrlEvent` (for callbacks) + - Find `I[Feature]Controller` (for actions/queries) + +3. **Implement the event listener**: + - Create class inheriting from `I[Feature]Event` + - Implement ALL pure virtual methods (use [Interface Methods Guide](../references/interface-methods.md)) + - Add logging to see when events fire + +4. **Get the controller**: + - Call `meetingService->Get[Feature]Controller()` + - Check for `nullptr` (might not be available) + +5. **Register listener and use**: + - Call `controller->SetEvent(new YourListener())` + - Call controller methods to use features + +6. **Test incrementally**: + - Start with event logging only + - Verify events fire correctly + - Add action methods one at a time + +--- + +## Complete Working Example: Mute Bot + +This bot auto-mutes when joining and responds to host unmute requests: + +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class MuteBotAudioListener : public IMeetingAudioCtrlEvent { +private: + IMeetingAudioController* audioCtrl; + +public: + MuteBotAudioListener(IMeetingAudioController* ctrl) : audioCtrl(ctrl) {} + + void onUserAudioStatusChange(IList* lstAudioStatusChange) override { + // Just log for debugging + std::cout << "[AUDIO] Audio status changed" << std::endl; + } + + void onUserActiveAudioChange(IList* plstActiveAudioUser) override { + // Not needed for this bot + } + + void onHostRequestStartAudio(IRequestStartAudioHandler* handler) override { + std::cout << "[AUDIO] Host requested unmute - auto accepting" << std::endl; + if (handler) { + handler->Accept(); // Automatically accept host request + } + } + + void onMuteOnEntryStatusChange(bool bEnabled) override { + std::cout << "[AUDIO] Mute on entry: " << bEnabled << std::endl; + } +}; + +void SetupMuteBot() { + // Get audio controller + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + if (!audioCtrl) return; + + // Register event listener + audioCtrl->SetEvent(new MuteBotAudioListener(audioCtrl)); + + // Join VoIP + audioCtrl->JoinVoip(); + + // Mute self immediately + audioCtrl->MuteAudio(0, true); + + std::cout << "[BOT] Mute bot active - will auto-accept host unmute requests" << std::endl; +} +``` + +--- + +## Advanced Pattern: Multiple Feature Integration + +Real bots typically use multiple controllers: + +```cpp +void SetupAdvancedBot() { + // Audio: Auto-mute on join + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + audioCtrl->SetEvent(new AudioEventListener()); + audioCtrl->JoinVoip(); + audioCtrl->MuteAudio(0, true); + + // Video: Keep video off + IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + videoCtrl->SetEvent(new VideoEventListener()); + // Video off by default when joining with isVideoOff = true + + // Chat: Respond to commands + IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + chatCtrl->SetEvent(new ChatCommandListener()); + chatCtrl->SendChatTo(L"everyone", L"Bot is ready!"); + + // Participants: Track who joins/leaves + IMeetingParticipantsController* participantsCtrl = meetingService->GetMeetingParticipantsController(); + participantsCtrl->SetEvent(new ParticipantsEventListener()); + + // Recording: Capture meeting + IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); + if (recordingCtrl->CanStartRawRecording() == SDKERR_SUCCESS) { + recordingCtrl->StartRawRecording(); + } +} +``` + +--- + +## Common Patterns Across All Controllers + +### Pattern 1: Check Availability Before Use + +```cpp +IMeetingRecordingController* ctrl = meetingService->GetMeetingRecordingController(); +if (ctrl) { // Controller exists + SDKError canUse = ctrl->CanStartRawRecording(); + if (canUse == SDKERR_SUCCESS) { // Feature available + ctrl->StartRawRecording(); + } +} +``` + +### Pattern 2: User ID = 0 Often Means "Self" + +```cpp +audioCtrl->MuteAudio(0, true); // Mute self +audioCtrl->MuteAudio(userId, true); // Mute specific user +``` + +### Pattern 3: Event Listeners Are Optional + +```cpp +// Some features work without event listener +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +// Don't need SetEvent() if you only send messages, don't receive +chatCtrl->SendChatTo(L"everyone", L"Hello"); +``` + +### Pattern 4: IList Collections + +```cpp +IList* participantList = participantsCtrl->GetParticipantsList(); +int count = participantList->GetCount(); +for (int i = 0; i < count; i++) { + unsigned int userId = participantList->GetItem(i); + // Use userId... +} +``` + +--- + +## Troubleshooting + +### Controller is nullptr + +**Cause**: Feature not available in this SDK app type or meeting state + +**Solution**: +- Check if you're in a meeting (`MEETING_STATUS_INMEETING`) +- Verify SDK app permissions in Zoom Marketplace +- Some controllers only available to hosts/co-hosts + +### SetEvent() Does Nothing + +**Cause**: Forgot Windows message loop + +**Solution**: See [Windows Message Loop Guide](../troubleshooting/windows-message-loop.md) + +### Methods Return SDKERR_WRONG_USAGE + +**Cause**: Called at wrong time or insufficient permissions + +**Solution**: +- Check method documentation for prerequisites +- Verify you're in correct meeting state +- Check if you're host/co-host (if required) + +--- + +## Summary: The Universal Formula + +``` +1. Read header file → Find I[Feature]Controller and I[Feature]Event +2. Implement event listener → Inherit I[Feature]Event, implement all methods +3. Get controller → meetingService->Get[Feature]Controller() +4. Register listener → controller->SetEvent(new YourListener()) +5. Use features → Call controller methods +``` + +**This pattern works for ALL 35+ controllers!** + +--- + +## Next Steps + +- Browse all available controllers: `SDK/x64/h/meeting_service_interface.h` (lines 1103-1314) +- Pick a feature you want to implement +- Find its header in `SDK/x64/h/meeting_service_components/` +- Follow the universal pattern above + +--- + +## Related Documentation + +- [Interface Methods Guide](../references/interface-methods.md) - How to implement event listeners +- [Authentication Pattern](../examples/authentication-pattern.md) - How to get meetingService +- [Raw Video Capture](../examples/raw-video-capture.md) - Example using recording controller +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Required for callbacks + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md new file mode 100644 index 00000000..b65b71e3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md @@ -0,0 +1,404 @@ +# Singleton Hierarchy: Navigation Guide + +## Overview + +The Zoom Windows Meeting SDK uses a **service locator pattern** - a tree of singletons where you navigate from root services down to specific features. You don't construct objects; you traverse to them. + +``` +You want to... You navigate to... +───────────────────────────────────────────────────── +Mute audio IMeetingService → IMeetingAudioController +Create breakout rooms IMeetingService → IMeetingBOController → IBOCreator +Control remote camera IMeetingService → IMeetingVideoController → IMeetingCameraHelper +Start live stream IMeetingService → IMeetingLiveStreamController +Add Q&A questions IMeetingService → IMeetingQAController +Enable interpretation IMeetingService → IMeetingInterpretationController +Batch invite contacts IAuthService → INotificationServiceHelper → IPresenceHelper → IBatchRequestContactHelper +``` + +--- + +## Complete Hierarchy (4 Levels Deep) + +``` +Level 0: Global Factory Functions (zoom_sdk.h) +│ +├─► Level 1: IAuthService +│ ├─► Level 2: IDirectShareServiceHelper [LEAF] +│ └─► Level 2: INotificationServiceHelper +│ └─► Level 3: IPresenceHelper +│ └─► Level 4: IBatchRequestContactHelper [LEAF - MAX DEPTH] +│ +├─► Level 1: IMeetingService +│ │ +│ │ ══════════════════════════════════════════════════════════════ +│ │ CROSS-PLATFORM CONTROLLERS (All platforms) +│ │ ══════════════════════════════════════════════════════════════ +│ │ +│ ├─► Level 2: IMeetingVideoController +│ │ ├─► Level 3: IMeetingCameraHelper [LEAF] +│ │ ├─► Level 3: ISetVideoOrderHelper [LEAF - Windows] +│ │ └─► Level 3: ICameraController [LEAF - Windows] +│ │ +│ ├─► Level 2: IMeetingAudioController [LEAF] +│ ├─► Level 2: IMeetingShareController [LEAF] +│ ├─► Level 2: IMeetingChatController [LEAF] +│ ├─► Level 2: IMeetingRecordingController [LEAF] +│ ├─► Level 2: IMeetingParticipantsController [LEAF] +│ ├─► Level 2: IMeetingWaitingRoomController [LEAF] +│ ├─► Level 2: IMeetingWebinarController [LEAF] +│ ├─► Level 2: IMeetingRawArchivingController [LEAF] +│ ├─► Level 2: IMeetingReminderController [LEAF] +│ ├─► Level 2: IMeetingEncryptionController [LEAF] +│ ├─► Level 2: IMeetingConfiguration [LEAF] +│ ├─► Level 2: IListFactory [LEAF - utility] +│ │ +│ ├─► Level 2: IMeetingBOController (Breakout Rooms) +│ │ ├─► Level 3: IBOCreator +│ │ │ └─► Level 4: IBatchCreateBOHelper [LEAF - MAX DEPTH] +│ │ ├─► Level 3: IBOAdmin [LEAF] +│ │ ├─► Level 3: IBOAssistant [LEAF] +│ │ ├─► Level 3: IBOAttendee [LEAF] +│ │ └─► Level 3: IBOData [LEAF] +│ │ +│ ├─► Level 2: IMeetingAICompanionController +│ │ ├─► Level 3: IMeetingSmartSummaryHelper [LEAF - DEPRECATED] +│ │ ├─► Level 3: IMeetingAICompanionSmartSummaryHelper [LEAF] +│ │ └─► Level 3: IMeetingAICompanionQueryHelper [LEAF] +│ │ +│ │ ══════════════════════════════════════════════════════════════ +│ │ WINDOWS-ONLY CONTROLLERS (#if defined(WIN32)) +│ │ ══════════════════════════════════════════════════════════════ +│ │ +│ ├─► Level 2: IMeetingUIController [LEAF - Windows] +│ │ +│ ├─► Level 2: IAnnotationController +│ │ └─► Level 3: ICustomizedAnnotationController [LEAF - Custom UI] +│ │ +│ ├─► Level 2: IMeetingRemoteController [LEAF - Windows] +│ ├─► Level 2: IMeetingH323Helper [LEAF - Windows] +│ ├─► Level 2: IMeetingPhoneHelper [LEAF - Windows] +│ ├─► Level 2: IMeetingLiveStreamController [LEAF - Windows] +│ ├─► Level 2: IClosedCaptionController [LEAF - Windows] +│ ├─► Level 2: IZoomRealNameAuthMeetingHelper [LEAF - Windows] +│ ├─► Level 2: IMeetingQAController [LEAF - Windows] +│ ├─► Level 2: IMeetingInterpretationController [LEAF - Windows] +│ ├─► Level 2: IMeetingSignInterpretationController [LEAF - Windows] +│ ├─► Level 2: IEmojiReactionController [LEAF - Windows] +│ ├─► Level 2: IMeetingAANController [LEAF - Windows] +│ ├─► Level 2: IMeetingWhiteboardController [LEAF - Windows] +│ ├─► Level 2: IMeetingDocsController [LEAF - Windows] +│ ├─► Level 2: IMeetingPollingController [LEAF - Windows] +│ ├─► Level 2: IMeetingRemoteSupportController [LEAF - Windows] +│ ├─► Level 2: IMeetingIndicatorController [LEAF - Windows] +│ ├─► Level 2: IMeetingProductionStudioController [LEAF - Windows] +│ │ +│ └─► Level 2: ICustomImmersiveController +│ └─► Level 3: ICustomImmersivePreLayoutHelper [LEAF] +│ +├─► Level 1: ISettingService +│ ├─► Level 2: IGeneralSettingContext [LEAF] +│ ├─► Level 2: IAudioSettingContext [LEAF] +│ ├─► Level 2: IVideoSettingContext [LEAF] +│ ├─► Level 2: IRecordingSettingContext [LEAF] +│ ├─► Level 2: IShareSettingContext [LEAF] +│ ├─► Level 2: IStatisticSettingContext [LEAF] +│ └─► Level 2: IWallpaperSettingContext [LEAF] +│ +├─► Level 1: INetworkConnectionHelper [LEAF] +│ +└─► Level 1: ICustomizedUIMgr (Custom UI Mode) + ├─► Level 2: ICustomizedVideoContainer (factory-created) + ├─► Level 2: ICustomizedShareRender (factory-created) + └─► Level 2: ICustomizedImmersiveContainer (factory-created) +``` + +--- + +## Controller Reference by Feature Domain + +### Cross-Platform Controllers + +| Controller | Getter Method | Purpose | +|------------|---------------|---------| +| `IMeetingVideoController` | `GetMeetingVideoController()` | Video on/off, spotlight, pin, virtual background | +| `IMeetingAudioController` | `GetMeetingAudioController()` | Mute/unmute, VoIP, audio device selection | +| `IMeetingShareController` | `GetMeetingShareController()` | Screen/app sharing, share settings | +| `IMeetingChatController` | `GetMeetingChatController()` | In-meeting chat, file transfer | +| `IMeetingRecordingController` | `GetMeetingRecordingController()` | Local/cloud recording control | +| `IMeetingParticipantsController` | `GetMeetingParticipantsController()` | User list, rename, remove, roles | +| `IMeetingWaitingRoomController` | `GetMeetingWaitingRoomController()` | Admit/deny users, waiting room settings | +| `IMeetingWebinarController` | `GetMeetingWebinarController()` | Webinar-specific controls, panelists | +| `IMeetingRawArchivingController` | `GetMeetingRawArchivingController()` | Raw archiving for compliance | +| `IMeetingReminderController` | `GetMeetingReminderController()` | Meeting reminders and notifications | +| `IMeetingEncryptionController` | `GetInMeetingEncryptionController()` | E2E encryption status | +| `IMeetingConfiguration` | `GetMeetingConfiguration()` | Meeting behavior configuration | +| `IMeetingBOController` | `GetMeetingBOController()` | Breakout rooms (has Level 3 helpers) | +| `IMeetingAICompanionController` | `GetMeetingAICompanionController()` | AI Companion features (has Level 3 helpers) | +| `IListFactory` | `GetListFactory()` | Factory for creating SDK list objects | + +### Windows-Only Controllers + +| Controller | Getter Method | Purpose | +|------------|---------------|---------| +| `IMeetingUIController` | `GetUIController()` | SDK UI window control, toolbar customization | +| `IAnnotationController` | `GetAnnotationController()` | Drawing/annotation on shared content | +| `IMeetingRemoteController` | `GetMeetingRemoteController()` | Remote control of shared content | +| `IMeetingH323Helper` | `GetH323Helper()` | H.323/SIP room system integration | +| `IMeetingPhoneHelper` | `GetMeetingPhoneHelper()` | PSTN dial-in/dial-out | +| `IMeetingLiveStreamController` | `GetMeetingLiveStreamController()` | YouTube/Facebook/custom RTMP streaming | +| `IClosedCaptionController` | `GetMeetingClosedCaptionController()` | Closed captions, live transcription | +| `IZoomRealNameAuthMeetingHelper` | `GetMeetingRealNameAuthController()` | China real-name authentication | +| `IMeetingQAController` | `GetMeetingQAController()` | Webinar Q&A feature | +| `IMeetingInterpretationController` | `GetMeetingInterpretationController()` | Language interpretation channels | +| `IMeetingSignInterpretationController` | `GetMeetingSignInterpretationController()` | Sign language interpretation | +| `IEmojiReactionController` | `GetMeetingEmojiReactionController()` | Emoji reactions (👍 🎉 etc.) | +| `IMeetingAANController` | `GetMeetingAANController()` | Advanced Audio Networking | +| `ICustomImmersiveController` | `GetMeetingImmersiveController()` | Immersive view/scenes (has Level 3 helper) | +| `IMeetingWhiteboardController` | `GetMeetingWhiteboardController()` | Collaborative whiteboard | +| `IMeetingDocsController` | `GetMeetingDocsController()` | In-meeting document sharing | +| `IMeetingPollingController` | `GetMeetingPollingController()` | Polls and quizzes | +| `IMeetingRemoteSupportController` | `GetMeetingRemoteSupportController()` | Remote support features | +| `IMeetingIndicatorController` | `GetMeetingIndicatorController()` | UI indicators and status | +| `IMeetingProductionStudioController` | `GetMeetingProductionStudioController()` | Production studio/broadcast features | + +--- + +## When to Use Each Level + +| Level | When | Example | +|-------|------|---------| +| **Level 1** | App startup, before joining | `CreateMeetingService()`, `CreateAuthService()` | +| **Level 2** | After joining meeting, for features | `meetingService->GetMeetingAudioController()` | +| **Level 3** | For specialized sub-features | `boController->GetBOCreatorHelper()` | +| **Level 4** | For batch/bulk operations | `boCreator->GetBatchCreateBOHelper()` | + +--- + +## How to Use (Universal Pattern) + +Every feature follows the **same 3-step pattern**: + +```cpp +// Step 1: Navigate to the controller (singleton) +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Step 2: Register event listener (observer pattern) +audioCtrl->SetEvent(new MyAudioEventListener()); + +// Step 3: Call methods +audioCtrl->MuteAudio(userId, true); +``` + +--- + +## Examples by Depth + +### Level 2 - Basic Feature (Audio) + +```cpp +// Get controller +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Use it +audioCtrl->JoinVoip(); +audioCtrl->MuteAudio(0, true); // 0 = self +``` + +### Level 3 - Sub-Feature (Breakout Room Creation) + +```cpp +// Navigate: Level 1 → Level 2 → Level 3 +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); + +// Use it +creator->CreateBreakoutRoom(L"Room 1"); +creator->AssignUserToBO(strUserID, strBOID); +``` + +### Level 4 - Batch Operations (Bulk Room Creation) + +```cpp +// Navigate: Level 1 → Level 2 → Level 3 → Level 4 +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); +IBatchCreateBOHelper* batch = creator->GetBatchCreateBOHelper(); + +// Use it (transaction pattern) +batch->CreateBOTransactionBegin(); +batch->AddNewBoToList(L"Room 1"); +batch->AddNewBoToList(L"Room 2"); +batch->AddNewBoToList(L"Room 3"); +batch->CreateBoTransactionCommit(); // Creates all 3 at once +``` + +--- + +## Why the Hierarchy Exists + +| Depth | Design Purpose | +|-------|----------------| +| **Level 1** (Services) | Lifecycle management - created once, destroyed at cleanup | +| **Level 2** (Controllers) | Feature grouping - one controller per domain | +| **Level 3** (Helpers) | Role-based access - different helpers for host vs attendee | +| **Level 4** (Batch) | Performance optimization - bulk ops instead of N individual calls | + +--- + +## Practical Rules + +### 1. Don't Cache Too Early + +Controllers return `nullptr` if not in meeting: + +```cpp +// WRONG - cached before meeting joined +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +meetingService->Join(joinParam); +audioCtrl->MuteAudio(0, true); // audioCtrl might be nullptr! + +// RIGHT - get after joining +meetingService->Join(joinParam); +// ... wait for MEETING_STATUS_INMEETING callback ... +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +if (audioCtrl) { + audioCtrl->MuteAudio(0, true); +} +``` + +### 2. Re-get After State Changes + +After joining/leaving meeting, get controllers again - previous pointers may be invalid. + +### 3. Check for nullptr + +Some helpers only available for hosts: + +```cpp +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); +if (creator) { + // Only hosts get a valid creator + creator->CreateBreakoutRoom(L"Room 1"); +} +``` + +### 4. Batch When Possible + +Level 4 helpers exist specifically for performance: + +```cpp +// SLOW - 10 individual calls +for (int i = 0; i < 10; i++) { + creator->CreateBreakoutRoom(roomNames[i]); +} + +// FAST - 1 batch call +IBatchCreateBOHelper* batch = creator->GetBatchCreateBOHelper(); +batch->CreateBOTransactionBegin(); +for (int i = 0; i < 10; i++) { + batch->AddNewBoToList(roomNames[i]); +} +batch->CreateBoTransactionCommit(); +``` + +--- + +## Deepest Paths (Maximum Depth = 4) + +| Path | Use Case | +|------|----------| +| `IMeetingService` → `IMeetingBOController` → `IBOCreator` → `IBatchCreateBOHelper` | Bulk breakout room creation | +| `IAuthService` → `INotificationServiceHelper` → `IPresenceHelper` → `IBatchRequestContactHelper` | Bulk contact operations | + +--- + +## Quick Reference: Common Navigation Paths + +### Core Meeting Features + +| Feature | Navigation Path | +|---------|-----------------| +| Audio control | `IMeetingService` → `GetMeetingAudioController()` | +| Video control | `IMeetingService` → `GetMeetingVideoController()` | +| Screen sharing | `IMeetingService` → `GetMeetingShareController()` | +| Chat | `IMeetingService` → `GetMeetingChatController()` | +| Recording | `IMeetingService` → `GetMeetingRecordingController()` | +| Participants | `IMeetingService` → `GetMeetingParticipantsController()` | +| Waiting room | `IMeetingService` → `GetMeetingWaitingRoomController()` | +| Breakout rooms | `IMeetingService` → `GetMeetingBOController()` → `GetBO*Helper()` | +| AI Companion | `IMeetingService` → `GetMeetingAICompanionController()` | +| AI Smart Summary | `IMeetingService` → `GetMeetingAICompanionController()` → `GetMeetingAICompanionSmartSummaryHelper()` | +| AI Query | `IMeetingService` → `GetMeetingAICompanionController()` → `GetMeetingAICompanionQueryHelper()` | +| Remote camera | `IMeetingService` → `GetMeetingVideoController()` → `GetMeetingCameraHelper()` | +| Video order (gallery) | `IMeetingService` → `GetMeetingVideoController()` → `GetSetVideoOrderHelper()` | +| Local camera device | `IMeetingService` → `GetMeetingVideoController()` → `GetMyCameraController()` | + +### Windows-Only Features + +| Feature | Navigation Path | +|---------|-----------------| +| Live streaming | `IMeetingService` → `GetMeetingLiveStreamController()` | +| Q&A (webinars) | `IMeetingService` → `GetMeetingQAController()` | +| Interpretation | `IMeetingService` → `GetMeetingInterpretationController()` | +| Sign language | `IMeetingService` → `GetMeetingSignInterpretationController()` | +| Closed captions | `IMeetingService` → `GetMeetingClosedCaptionController()` | +| Annotations | `IMeetingService` → `GetAnnotationController()` | +| Annotations (Custom UI) | `IMeetingService` → `GetAnnotationController()` → `GetCustomizedAnnotationController()` | +| Emoji reactions | `IMeetingService` → `GetMeetingEmojiReactionController()` | +| Polling | `IMeetingService` → `GetMeetingPollingController()` | +| Whiteboard | `IMeetingService` → `GetMeetingWhiteboardController()` | +| Docs | `IMeetingService` → `GetMeetingDocsController()` | +| H.323/SIP | `IMeetingService` → `GetH323Helper()` | +| Phone dial-in/out | `IMeetingService` → `GetMeetingPhoneHelper()` | +| Remote control | `IMeetingService` → `GetMeetingRemoteController()` | +| Immersive view | `IMeetingService` → `GetMeetingImmersiveController()` | +| UI control | `IMeetingService` → `GetUIController()` | + +### Settings & Pre-Meeting + +| Feature | Navigation Path | +|---------|-----------------| +| Audio settings | `ISettingService` → `GetAudioSettings()` | +| Video settings | `ISettingService` → `GetVideoSettings()` | +| Recording settings | `ISettingService` → `GetRecordingSettings()` | +| Share settings | `ISettingService` → `GetShareSettings()` | +| Presence/contacts | `IAuthService` → `GetNotificationServiceHelper()` → `GetPresenceHelper()` | + +--- + +## Deprecated Controllers & Helpers + +| Deprecated | Replacement | +|------------|-------------| +| `IMeetingSmartSummaryController` | Use `IMeetingAICompanionController` | +| `IMeetingSmartSummaryHelper` | Use `IMeetingAICompanionSmartSummaryHelper` via `GetMeetingAICompanionSmartSummaryHelper()` | + +--- + +## Platform Availability Summary + +| Category | Count | Platform | +|----------|-------|----------| +| Cross-platform controllers | 15 | Windows, macOS, Linux | +| Windows-only controllers | 20 | Windows only (`#if defined(WIN32)`) | +| **Total** | **35** | — | + +> **Note**: When developing cross-platform apps, use `#if defined(WIN32)` guards around Windows-only controller access. + +--- + +## Related Documentation + +- [SDK Architecture Pattern](sdk-architecture-pattern.md) - The universal 3-step pattern +- [Custom UI Architecture](custom-ui-architecture.md) - Custom UI specific hierarchy +- [Breakout Rooms Example](../examples/breakout-rooms.md) - Level 3 helpers in action +- [Chat Example](../examples/chat.md) - IMeetingChatController usage +- [Captions/Transcription Example](../examples/captions-transcription.md) - IClosedCaptionController usage +- [Local Recording Example](../examples/local-recording.md) - IMeetingRecordingController usage +- [Video Advanced Example](../examples/video-advanced.md) - Camera control, video order (Level 3 helpers) +- [AI Companion Example](../examples/ai-companion.md) - Smart Summary, AI Query (Level 3 helpers) + +--- + +**TL;DR**: The hierarchy is your navigation map. Start at a service, drill down to the feature you need, then call methods. Deeper levels = more specialized operations. Windows has 20 additional controllers not available on other platforms. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/ai-companion.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/ai-companion.md new file mode 100644 index 00000000..e398859a --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/ai-companion.md @@ -0,0 +1,539 @@ +# AI Companion Features: Smart Summary & Query + +## Overview + +The `IMeetingAICompanionController` provides Level 3 sub-helpers for AI-powered meeting features: + +| Helper | Getter | Status | Purpose | +|--------|--------|--------|---------| +| `IMeetingSmartSummaryHelper` | `GetMeetingSmartSummaryHelper()` | **DEPRECATED** | Legacy smart summary API | +| `IMeetingAICompanionSmartSummaryHelper` | `GetMeetingAICompanionSmartSummaryHelper()` | Current | AI-powered meeting summaries | +| `IMeetingAICompanionQueryHelper` | `GetMeetingAICompanionQueryHelper()` | Current | AI Q&A about meeting content | + +--- + +## Navigation Path + +``` +IMeetingService + └─► GetMeetingAICompanionController() + ├─► GetMeetingSmartSummaryHelper() // DEPRECATED + ├─► GetMeetingAICompanionSmartSummaryHelper() // Smart Summary + └─► GetMeetingAICompanionQueryHelper() // AI Query/Q&A +``` + +--- + +## Prerequisites + +AI Companion features require: +1. Feature enabled in Zoom account settings +2. Host privileges (for starting features) +3. Meeting in progress (`MEETING_STATUS_INMEETING`) + +--- + +## 1. Smart Summary (IMeetingAICompanionSmartSummaryHelper) + +AI-generated meeting summaries that capture key points, action items, and decisions. + +### Get the Helper + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); +if (!aiCtrl) return; + +IMeetingAICompanionSmartSummaryHelper* summaryHelper = + aiCtrl->GetMeetingAICompanionSmartSummaryHelper(); +if (!summaryHelper) return; +``` + +### Set Up Event Handler + +```cpp +class SmartSummaryEventHandler : public IMeetingAICompanionSmartSummaryHelperEvent { +public: + void onSmartSummaryStateNotSupported() override { + // Meeting doesn't support smart summary + // Account may not have the feature enabled + } + + void onSmartSummaryStateSupportedButDisabled( + IMeetingEnableSmartSummaryHandler* handler) override + { + // Feature supported but needs to be enabled + if (handler) { + if (handler->IsForRequest()) { + // This is a request from another user + // Host should decide whether to enable + } else { + // Current user can enable directly + handler->EnableSmartSummary(); + } + } + } + + void onSmartSummaryStateEnabledButNotStarted( + IMeetingStartSmartSummaryHandler* handler) override + { + // Feature enabled, ready to start + if (handler) { + if (handler->IsForRequest()) { + // Request from another user to start + } else { + // Can start directly + handler->StartSmartSummary(); + } + } + } + + void onSmartSummaryStateStarted( + IMeetingStopSmartSummaryHandler* handler) override + { + // Smart summary is now active + // handler is nullptr if current user can't stop it + if (handler) { + // Can stop if needed + // handler->StopSmartSummary(); + } + } + + void onFailedToStartSmartSummary(bool bTimeout) override { + if (bTimeout) { + // Request timed out + } else { + // Host/cohost declined the request + } + } + + void onSmartSummaryEnableRequestReceived( + IMeetingApproveEnableSmartSummaryHandler* handler) override + { + // Another user requested to enable smart summary + // Host/cohost receives this callback + if (handler) { + unsigned int requesterId = handler->GetSenderUserID(); + // Approve or handle the request + handler->ContinueApprove(); + } + } + + void onSmartSummaryStartRequestReceived( + IMeetingApproveStartSmartSummaryHandler* handler) override + { + // Another user requested to start smart summary + if (handler) { + unsigned int requesterId = handler->GetSenderUserID(); + handler->Approve(); // or handler->Decline(); + } + } + + void onSmartSummaryEnableActionCallback( + IMeetingEnableSmartSummaryActionHandler* handler) override + { + // Confirmation dialog for enabling smart summary + if (handler) { + const zchar_t* title = handler->GetTipTitle(); + const zchar_t* tip = handler->GetTipString(); + // Show UI with tip, then: + handler->Confirm(); // or handler->Cancel(); + } + } +}; + +// Register the handler +SmartSummaryEventHandler summaryHandler; +summaryHelper->SetEvent(&summaryHandler); +``` + +--- + +## 2. AI Query (IMeetingAICompanionQueryHelper) + +Ask questions about meeting content and get AI-generated answers. + +### Get the Helper + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); +if (!aiCtrl) return; + +IMeetingAICompanionQueryHelper* queryHelper = + aiCtrl->GetMeetingAICompanionQueryHelper(); +if (!queryHelper) return; +``` + +### Set Up Event Handler + +```cpp +class AIQueryEventHandler : public IMeetingAICompanionQueryHelperEvent { +public: + void onQueryStateNotSupported() override { + // Meeting doesn't support AI query + } + + void onQueryStateSupportedButDisabled( + IMeetingEnableQueryHandler* pHandler) override + { + // Query supported but disabled + if (pHandler && !pHandler->IsForRequest()) { + pHandler->EnableQuery(); + } + } + + void onQueryStateEnabledButNotStarted( + IMeetingStartQueryHandler* pHandler) override + { + // Query enabled, ready to start + if (pHandler && !pHandler->IsForRequest()) { + pHandler->StartMeetingQuery(); + } + } + + void onQueryStateStarted(IMeetingSendQueryHandler* pHandler) override { + // Query is active - can now send questions + if (pHandler) { + // Get suggested questions + IList* defaultQuestions = + pHandler->GetDefaultQueryQuestions(); + if (defaultQuestions) { + for (int i = 0; i < defaultQuestions->GetCount(); i++) { + const zchar_t* question = defaultQuestions->GetItem(i); + // Display in UI + } + } + + // Check if we can send queries + if (pHandler->CanSendQuery()) { + // Send a question + pHandler->SendQueryQuestion(L"What were the main action items?"); + } else { + // Need to request privilege + pHandler->RequestSendQueryPrivilege(); + } + } + } + + void onReceiveQueryAnswer(IMeetingAICompanionQueryItem* pQueryItem) override { + // Received an answer to our question + if (pQueryItem) { + const zchar_t* queryId = pQueryItem->GetQueryID(); + const zchar_t* question = pQueryItem->GetQustionContent(); + const zchar_t* answer = pQueryItem->GetAnswerContent(); + time_t timestamp = pQueryItem->GetTimeStamp(); + + MeetingAICompanionQueryRequestError errorCode = pQueryItem->GetErrorCode(); + if (errorCode == MeetingAICompanionQueryRequestError_OK) { + // Display answer to user + + // Send feedback if user indicates quality + // pQueryItem->Feedback(MeetingAICompanionQueryFeedbackType_Good); + // or + // pQueryItem->Feedback(MeetingAICompanionQueryFeedbackType_Bad); + } else { + // Handle error + const zchar_t* errorMsg = pQueryItem->GetErrorMsg(); + } + } + } + + void onQuerySettingChanged(MeetingAICompanionQuerySettingOptions eSetting) override { + // Query settings changed + switch (eSetting) { + case MeetingAICompanionQuerySettingOptions_WhenQueryStarted: + // All can ask about discussions since AI started + break; + case MeetingAICompanionQuerySettingOptions_WhenParticipantsJoin: + // Can ask about discussions since they joined + break; + case MeetingAICompanionQuerySettingOptions_OnlyHost: + // Only host can ask questions + break; + // ... handle other settings + } + } + + void onFailedToStartQuery(bool bTimeout) override { + if (bTimeout) { + // Request timed out + } else { + // Request declined + } + } + + void onSendQueryPrivilegeChanged(bool canSendQuery) override { + // Privilege to send queries changed + if (canSendQuery) { + // Can now send questions + } else { + // Lost ability to send questions + } + } + + void onFailedToRequestSendQuery(bool bTimeout) override { + // Failed to get send query privilege + } + + // ... other callbacks for request handling + void onReceiveRequestToEnableQuery( + IMeetingApproveEnableQueryHandler* pHandler) override {} + void onReceiveRequestToStartQuery( + IMeetingApproveStartQueryHandler* pHandler) override {} + void onQueryEnableActionCallback( + IMeetingEnableQueryActionHandler* pHandler) override {} + void onReceiveRequestToSendQuery( + IMeetingApproveSendQueryHandler* pHandler) override {} +}; + +// Register the handler +AIQueryEventHandler queryHandler; +queryHelper->SetEvent(&queryHandler); +``` + +### Change Query Settings (Host Only) + +```cpp +bool canChange = false; +SDKError err = queryHelper->CanChangeQuerySetting(canChange); +if (err == SDKERR_SUCCESS && canChange) { + // Set who can ask questions + queryHelper->ChangeQuerySettings( + MeetingAICompanionQuerySettingOptions_WhenParticipantsJoin); + + // Get current setting + MeetingAICompanionQuerySettingOptions currentSetting = + queryHelper->GetSelectedQuerySetting(); +} +``` + +### Legal Notices + +```cpp +bool isAvailable = false; +queryHelper->IsAICompanionQueryLegalNoticeAvailable(isAvailable); +if (isAvailable) { + const zchar_t* prompt = queryHelper->GetAICompanionQueryLegalNoticesPrompt(); + const zchar_t* explained = queryHelper->GetAICompanionQueryLegalNoticesExplained(); + // Display legal notice to user +} +``` + +--- + +## 3. Controller-Level Operations + +The main controller provides global AI Companion controls. + +### Turn All AI Features On/Off + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); + +// Check support +if (aiCtrl->IsTurnoffAllAICompanionsSupported()) { + // Can turn off all AI features + if (aiCtrl->CanTurnOffAllAICompanions()) { + bool deleteAssets = false; // Keep or delete meeting assets + aiCtrl->TurnOffAllAICompanions(deleteAssets); + } +} + +if (aiCtrl->IsTurnOnAllAICompanionsSupported()) { + // Can turn on all AI features + if (aiCtrl->CanTurnOnAllAICompanions()) { + aiCtrl->TurnOnAllAICompanions(); + } +} +``` + +### Request Host to Toggle AI Features + +```cpp +// For non-host users +if (aiCtrl->CanRequestTurnoffAllAICompanions()) { + aiCtrl->RequestTurnoffAllAICompanions(); +} + +if (aiCtrl->CanRequestTurnOnAllAICompanions()) { + aiCtrl->RequestTurnOnAllAICompanions(); +} +``` + +### Controller Event Handler + +```cpp +class AICompanionCtrlEventHandler : public IMeetingAICompanionCtrlEvent { +public: + void onAICompanionFeatureTurnOffByParticipant( + IAICompanionFeatureTurnOnAgainHandler* handler) override + { + // Participant turned off AI feature before host joined + if (handler) { + IList* features = handler->GetFeatureList(); + IList* deletedAssets = + handler->GetAssetsDeletedFeatureList(); + + // Host can turn features back on + handler->TurnOnAgain(); + // Or agree to keep them off + // handler->AgreeTurnOff(); + } + } + + void onAICompanionFeatureSwitchRequested( + IAICompanionFeatureSwitchHandler* handler) override + { + // User requested to toggle AI features + if (handler) { + unsigned int requesterId = handler->GetRequestUserID(); + bool isTurningOn = handler->IsTurnOn(); + + bool deleteAssets = false; + handler->Agree(deleteAssets); + // or handler->Decline(); + } + } + + void onAICompanionFeatureSwitchRequestResponse( + bool bTimeout, bool bAgree, bool bTurnOn) override + { + // Response to our request + if (!bTimeout && bAgree) { + // Request approved + } + } + + void onAICompanionFeatureCanNotBeTurnedOff( + IList* features) override + { + // These features cannot be turned off + if (features) { + for (int i = 0; i < features->GetCount(); i++) { + AICompanionFeature feature = features->GetItem(i); + // Handle each feature + } + } + } + + void onHostUnsupportedStopNotesRequest() override { + // Host's client doesn't support stopping Notes + } +}; + +// Register controller event handler +AICompanionCtrlEventHandler ctrlHandler; +aiCtrl->SetEvent(&ctrlHandler); +``` + +--- + +## Complete Example: Smart Summary Flow + +```cpp +class SmartSummaryManager { +private: + IMeetingAICompanionController* m_aiCtrl = nullptr; + IMeetingAICompanionSmartSummaryHelper* m_summaryHelper = nullptr; + + class SummaryHandler : public IMeetingAICompanionSmartSummaryHelperEvent { + public: + SmartSummaryManager* m_manager = nullptr; + + void onSmartSummaryStateNotSupported() override { + m_manager->OnFeatureNotSupported(); + } + + void onSmartSummaryStateSupportedButDisabled( + IMeetingEnableSmartSummaryHandler* handler) override { + if (handler && !handler->IsForRequest()) { + handler->EnableSmartSummary(); + } + } + + void onSmartSummaryStateEnabledButNotStarted( + IMeetingStartSmartSummaryHandler* handler) override { + if (handler && !handler->IsForRequest()) { + handler->StartSmartSummary(); + } + } + + void onSmartSummaryStateStarted( + IMeetingStopSmartSummaryHandler* handler) override { + m_manager->OnSummaryStarted(handler); + } + + void onFailedToStartSmartSummary(bool bTimeout) override { + m_manager->OnStartFailed(bTimeout); + } + + void onSmartSummaryEnableRequestReceived( + IMeetingApproveEnableSmartSummaryHandler* handler) override { + if (handler) handler->ContinueApprove(); + } + + void onSmartSummaryStartRequestReceived( + IMeetingApproveStartSmartSummaryHandler* handler) override { + if (handler) handler->Approve(); + } + + void onSmartSummaryEnableActionCallback( + IMeetingEnableSmartSummaryActionHandler* handler) override { + if (handler) handler->Confirm(); + } + }; + + SummaryHandler m_handler; + IMeetingStopSmartSummaryHandler* m_stopHandler = nullptr; + +public: + bool Initialize(IMeetingService* meetingService) { + m_aiCtrl = meetingService->GetMeetingAICompanionController(); + if (!m_aiCtrl) return false; + + m_summaryHelper = m_aiCtrl->GetMeetingAICompanionSmartSummaryHelper(); + if (!m_summaryHelper) return false; + + m_handler.m_manager = this; + m_summaryHelper->SetEvent(&m_handler); + return true; + } + + void OnFeatureNotSupported() { + // Update UI - feature not available + } + + void OnSummaryStarted(IMeetingStopSmartSummaryHandler* handler) { + m_stopHandler = handler; + // Update UI - summary is recording + } + + void OnStartFailed(bool timeout) { + // Show error message + } + + void StopSummary() { + if (m_stopHandler) { + m_stopHandler->StopSmartSummary(); + m_stopHandler = nullptr; + } + } +}; +``` + +--- + +## AI Companion Features Summary + +| Feature | Description | Assets Generated | +|---------|-------------|------------------| +| `SMART_SUMMARY` | AI meeting summary | Summary document | +| `QUERY` | AI Q&A about meeting | Transcript | +| `SMART_RECORDING` | AI-enhanced recording | Recording with AI insights | + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Complete navigation map +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern +- [Captions & Transcription](captions-transcription.md) - Related live transcription features diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/authentication-pattern.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/authentication-pattern.md new file mode 100644 index 00000000..d0268b30 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/authentication-pattern.md @@ -0,0 +1,702 @@ +# Authentication Flow Pattern + +Complete guide to authenticating with the Zoom Windows SDK using JWT tokens. + +--- + +## Overview + +Authentication is the first required step before joining meetings. The SDK uses JWT (JSON Web Token) authentication for Meeting SDK apps. + +### Flow Sequence + +``` +1. Initialize SDK (InitSDK) +2. [OPTIONAL] Register Network Connection Handler for proxy detection + 2a. Wait for onProxyDetectComplete() callback +3. Create Auth Service (CreateAuthService) +4. Set Event Listener (SetEvent) +5. Call SDKAuth with JWT token +6. Process Windows messages (CRITICAL!) +7. Wait for onAuthenticationReturn callback +8. Create Meeting Service (CreateMeetingService) +9. Join/Start meeting +10. Wait for MEETING_STATUS_INMEETING callback +11. NOW safe to use controllers (GetMeetingAudioController, etc.) +``` + +### State Machine + +``` +┌──────────────┐ InitSDK() ┌──────────────┐ +│ UNINITIALIZED │ ─────────────► │ INITIALIZED │ +└──────────────┘ └───────┬──────┘ + │ + │ CreateAuthService() + SDKAuth() + ▼ +┌──────────────┐ onAuthenticationReturn ┌──────────────┐ +│ MEETING │ ◄─────────────────── │ AUTHENTICATING │ +│ READY │ (AUTHRET_SUCCESS) └──────────────┘ +└───────┬──────┘ + │ + │ Join() or Start() + ▼ +┌──────────────┐ onMeetingStatusChanged ┌──────────────┐ +│ IN MEETING │ ◄─────────────────── │ JOINING │ +│ (Controllers OK)│ (MEETING_STATUS_ └──────────────┘ +└──────────────┘ INMEETING) +``` + +--- + +## Complete Working Example + +```cpp +#include +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +// Global state +bool g_authenticated = false; +bool g_exit = false; +IAuthService* g_authService = nullptr; + +// Step 1: Create Event Listener +class MyAuthListener : public IAuthServiceEvent { +public: + void onAuthenticationReturn(AuthResult ret) override { + std::cout << "[AUTH] Callback received! Result: " << ret << std::endl; + + switch (ret) { + case AUTHRET_SUCCESS: + std::cout << "[AUTH] Authentication successful!" << std::endl; + g_authenticated = true; + break; + case AUTHRET_KEYORSECRETEMPTY: + std::cerr << "[AUTH] ERROR: SDK Key or Secret is empty" << std::endl; + break; + case AUTHRET_JWTTOKENWRONG: + std::cerr << "[AUTH] ERROR: JWT token is invalid" << std::endl; + break; + case AUTHRET_OVERTIME: + std::cerr << "[AUTH] ERROR: Authentication timeout" << std::endl; + break; + case AUTHRET_NETWORKISSUE: + std::cerr << "[AUTH] ERROR: Network connection issue" << std::endl; + break; + default: + std::cerr << "[AUTH] ERROR: Unknown error: " << ret << std::endl; + } + } + + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override { + // Not used for JWT auth + } + + void onLogout() override { + std::cout << "[AUTH] Logged out" << std::endl; + } + + void onZoomIdentityExpired() override { + std::cout << "[AUTH] Zoom identity expired" << std::endl; + } + + void onZoomAuthIdentityExpired() override { + std::cout << "[AUTH] Zoom auth identity expired" << std::endl; + } + + #if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, + SDKNotificationServiceError error) override { + std::cout << "[AUTH] Notification service status: " << status << std::endl; + } + #endif +}; + +// Step 2: Initialize SDK +bool InitializeSDK() { + std::cout << "[1/3] Initializing SDK..." << std::endl; + + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + initParam.strSupportUrl = L"https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: InitSDK failed: " << err << std::endl; + return false; + } + + std::cout << "SDK initialized successfully" << std::endl; + return true; +} + +// Step 3: Authenticate +bool AuthenticateSDK(const std::wstring& jwt_token) { + std::cout << "[2/3] Authenticating..." << std::endl; + + // Create auth service + SDKError err = CreateAuthService(&g_authService); + if (err != SDKERR_SUCCESS || !g_authService) { + std::cerr << "ERROR: CreateAuthService failed: " << err << std::endl; + return false; + } + std::cout << "Auth service created" << std::endl; + + // Set event listener + err = g_authService->SetEvent(new MyAuthListener()); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: SetEvent failed: " << err << std::endl; + return false; + } + std::cout << "Event listener set" << std::endl; + + // Validate JWT token + if (jwt_token.empty()) { + std::cerr << "ERROR: JWT token is empty!" << std::endl; + return false; + } + std::cout << "JWT token length: " << jwt_token.length() << " characters" << std::endl; + + // Create auth context + AuthContext authContext; + authContext.jwt_token = jwt_token.c_str(); + + // Call SDKAuth + err = g_authService->SDKAuth(authContext); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: SDKAuth failed: " << err << std::endl; + return false; + } + + std::cout << "SDKAuth called successfully, waiting for callback..." << std::endl; + return true; +} + +// Step 4: Wait for Authentication (WITH MESSAGE LOOP!) +bool WaitForAuthentication(int timeoutSeconds = 30) { + std::cout << "[3/3] Waiting for authentication..." << std::endl; + + auto startTime = std::chrono::steady_clock::now(); + + while (!g_authenticated && !g_exit) { + // CRITICAL: Process Windows messages for SDK callbacks! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + + if (elapsed >= timeoutSeconds) { + std::cerr << "ERROR: Authentication timeout after " << timeoutSeconds << " seconds" << std::endl; + return false; + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} + +// Main function +int main() { + // Your JWT token here + std::wstring jwt_token = L"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; + + // Step 1: Initialize SDK + if (!InitializeSDK()) { + return 1; + } + + // Step 2: Authenticate + if (!AuthenticateSDK(jwt_token)) { + return 1; + } + + // Step 3: Wait for callback + if (!WaitForAuthentication()) { + return 1; + } + + std::cout << "\n✓ Authentication complete! Ready to join meeting." << std::endl; + + // Now you can join meetings... + + // Cleanup + if (g_authService) { + DestroyAuthService(g_authService); + } + CleanUPSDK(); + + return 0; +} +``` + +--- + +## Optional: Network/Proxy Detection + +If your app needs to work behind corporate proxies, register the network connection handler **after InitSDK** but **before authentication**: + +```cpp +#include + +class MyNetworkHandler : public INetworkConnectionHandler { +public: + void onProxyDetectComplete() override { + std::cout << "[NETWORK] Proxy detection complete" << std::endl; + // NOW safe to proceed with authentication + g_proxyDetected = true; + } + + void onProxySettingNotification(IProxySettingHandler* handler) override { + // Handle proxy settings if needed + std::cout << "[NETWORK] Proxy settings notification" << std::endl; + } + + void onSSLCertVerifyNotification(ISSLCertVerificationHandler* handler) override { + // Handle SSL cert verification if needed + std::cout << "[NETWORK] SSL cert verification" << std::endl; + } +}; + +bool WaitForProxyDetection() { + // Create network helper + INetworkConnectionHelper* networkHelper = nullptr; + CreateNetworkConnectionHelper(&networkHelper); + + if (networkHelper) { + networkHelper->RegisterNetworkConnectionHandler(new MyNetworkHandler()); + + // Wait for onProxyDetectComplete callback + while (!g_proxyDetected) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return true; + } + return false; +} +``` + +> **When to use**: Only needed if you're behind a corporate proxy or need SSL certificate handling. Most apps can skip this step. + +--- + +## JWT Token Setup + +### Generating JWT Token + +1. **Go to Zoom Marketplace**: https://marketplace.zoom.us/ +2. **Create/Select App**: Go to "Develop" → "Build App" → "Meeting SDK" +3. **Get Credentials**: Find "App Credentials" tab + - SDK Key (Client ID) + - SDK Secret (Client Secret) +4. **Generate JWT**: Use the built-in JWT generator or create manually + +### JWT Token Format + +Valid JWT token: +- **Length**: 200-500 characters +- **Starts with**: `eyJ` +- **Contains**: Two periods (`.`) separating three parts +- **Example**: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBLZXk...` + +### Loading from Configuration File + +```cpp +#include +#include + +bool LoadJWTFromConfig(std::wstring& jwt_token) { + std::ifstream f("config.json"); + if (!f.is_open()) { + std::cerr << "ERROR: config.json not found" << std::endl; + return false; + } + + Json::Value config; + try { + f >> config; + } catch (const std::exception& e) { + std::cerr << "ERROR: Failed to parse config.json: " << e.what() << std::endl; + return false; + } + + if (config["sdk_jwt"].empty()) { + std::cerr << "ERROR: sdk_jwt not found in config.json" << std::endl; + return false; + } + + std::string jwt_str = config["sdk_jwt"].asString(); + jwt_token = std::wstring(jwt_str.begin(), jwt_str.end()); + + return true; +} +``` + +**config.json**: +```json +{ + "sdk_jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "meeting_number": "1234567890", + "passcode": "meeting_password" +} +``` + +--- + +## Common Authentication Errors + +### AuthResult Error Codes + +| Code | Enum | Meaning | Solution | +|------|------|---------|----------| +| 0 | `AUTHRET_SUCCESS` | ✓ Success | Continue to join meeting | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | SDK Key/Secret empty | Check JWT token | +| 3 | `AUTHRET_JWTTOKENWRONG` | Invalid JWT | Regenerate token | +| 4 | `AUTHRET_OVERTIME` | Request timeout | Check network | +| 5 | `AUTHRET_NETWORKISSUE` | Network problem | Check firewall/internet | +| 7 | `AUTHRET_CLIENT_INCOMPATIBLE` | SDK version mismatch | Update SDK | +| 10 | `AUTHRET_JWTTOKENEXPIRED` | JWT expired | Generate fresh token | + +### Timeout (No Callback) + +**Symptom**: Waiting forever, no callback received + +**Causes**: +1. **Missing Windows message loop** (most common!) +2. Network/firewall blocking +3. Invalid JWT format + +**Debug**: +```cpp +void onAuthenticationReturn(AuthResult ret) override { + std::cout << "CALLBACK FIRED! Result: " << ret << std::endl; // Does this print? +} +``` + +If you never see "CALLBACK FIRED!", you're not processing Windows messages! + +### Invalid JWT Token + +**Symptom**: `AUTHRET_JWTTOKENWRONG` (code 3) + +**Causes**: +1. Token expired (typically 24-48 hours) +2. Wrong SDK Key/Secret used to generate token +3. Token generated for different app +4. Malformed token + +**Solution**: +- Generate fresh JWT token from Zoom Marketplace +- Verify SDK credentials match +- Check token format (starts with "eyJ", has two dots) + +### Network Issues + +**Symptom**: `AUTHRET_NETWORKISSUE` (code 5) or timeout + +**Solutions**: +- Check internet connection +- Verify can access zoom.us from browser +- Check Windows Firewall settings +- If behind corporate proxy, may need proxy configuration +- Temporarily disable antivirus to test + +--- + +## Best Practices + +### 1. Validate JWT Before Using + +```cpp +bool ValidateJWT(const std::wstring& jwt_token) { + // Check length + if (jwt_token.length() < 100) { + std::cerr << "JWT token too short: " << jwt_token.length() << std::endl; + return false; + } + + // Check starts with "eyJ" + if (jwt_token.substr(0, 3) != L"eyJ") { + std::cerr << "JWT token doesn't start with 'eyJ'" << std::endl; + return false; + } + + // Check has two dots (three parts) + int dotCount = 0; + for (wchar_t c : jwt_token) { + if (c == L'.') dotCount++; + } + if (dotCount != 2) { + std::cerr << "JWT token should have 2 dots, found: " << dotCount << std::endl; + return false; + } + + return true; +} +``` + +### 2. Add Timeout with Progress Updates + +```cpp +bool WaitForAuthenticationWithProgress(int timeoutSeconds = 30) { + auto startTime = std::chrono::steady_clock::now(); + int lastProgressSeconds = 0; + + while (!g_authenticated && !g_exit) { + // Process messages + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Show progress every 5 seconds + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + + if (elapsed > lastProgressSeconds && elapsed % 5 == 0) { + std::cout << "Still waiting... (" << elapsed << "s)" << std::endl; + lastProgressSeconds = elapsed; + } + + if (elapsed >= timeoutSeconds) { + return false; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} +``` + +### 3. Cleanup on Failure + +```cpp +void Cleanup() { + if (g_authService) { + DestroyAuthService(g_authService); + g_authService = nullptr; + } + CleanUPSDK(); +} + +int main() { + if (!InitializeSDK()) { + return 1; + } + + if (!AuthenticateSDK(jwt_token)) { + Cleanup(); // Always cleanup on failure + return 1; + } + + if (!WaitForAuthentication()) { + Cleanup(); + return 1; + } + + // ... use SDK ... + + Cleanup(); // Cleanup on success too + return 0; +} +``` + +--- + +## After Authentication: Join/Start Meeting + +Once authenticated, create the meeting service and join: + +```cpp +#include + +IMeetingService* g_meetingService = nullptr; +bool g_inMeeting = false; + +class MyMeetingListener : public IMeetingServiceEvent { +public: + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + std::cout << "[MEETING] Status changed: " << status << std::endl; + + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "[MEETING] Idle" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "[MEETING] Connecting..." << std::endl; + break; + case MEETING_STATUS_WAITINGFORHOST: + std::cout << "[MEETING] Waiting for host..." << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "[MEETING] IN MEETING - Controllers now available!" << std::endl; + g_inMeeting = true; + SetupControllers(); // NOW safe to get controllers + break; + case MEETING_STATUS_DISCONNECTING: + std::cout << "[MEETING] Disconnecting..." << std::endl; + break; + case MEETING_STATUS_ENDED: + std::cout << "[MEETING] Ended" << std::endl; + g_inMeeting = false; + break; + case MEETING_STATUS_FAILED: + std::cerr << "[MEETING] FAILED - Error: " << iResult << std::endl; + break; + } + } + + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool active) override {} + void onMeetingTopicChanged(const zchar_t* topic) override {} + void onMeetingFullToWatchLiveStream(const zchar_t* url) override {} + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality quality, + unsigned int userId, bool uplink) override {} +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* handler) override {} +#endif +}; + +bool CreateAndJoinMeeting(UINT64 meetingNumber, const wchar_t* passcode, const wchar_t* userName) { + // Step 1: Create meeting service + SDKError err = CreateMeetingService(&g_meetingService); + if (err != SDKERR_SUCCESS || !g_meetingService) { + std::cerr << "ERROR: CreateMeetingService failed: " << err << std::endl; + return false; + } + + // Step 2: Set event listener + g_meetingService->SetEvent(new MyMeetingListener()); + + // Step 3: Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; + param.meetingNumber = meetingNumber; + param.userName = userName; + param.psw = passcode; + param.isVideoOff = true; // Bot typically joins with video off + param.isAudioOff = false; // But audio on to hear + + // Step 4: Join! + err = g_meetingService->Join(joinParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: Join failed: " << err << std::endl; + return false; + } + + std::cout << "Join() called, waiting for MEETING_STATUS_INMEETING..." << std::endl; + return true; +} + +// Call this ONLY after MEETING_STATUS_INMEETING +void SetupControllers() { + // Audio + IMeetingAudioController* audioCtrl = g_meetingService->GetMeetingAudioController(); + if (audioCtrl) { + audioCtrl->JoinVoip(); + audioCtrl->MuteAudio(0, true); // Mute self + } + + // Video + IMeetingVideoController* videoCtrl = g_meetingService->GetMeetingVideoController(); + // ... + + // Chat + IMeetingChatController* chatCtrl = g_meetingService->GetMeetingChatController(); + // ... +} +``` + +### CRITICAL: Controller Availability + +| When | Controllers Available? | +|------|----------------------| +| Before `MEETING_STATUS_INMEETING` | **NO** - Returns `nullptr` | +| After `MEETING_STATUS_INMEETING` | **YES** - Safe to use | +| After `MEETING_STATUS_ENDED` | **NO** - Pointers invalid | + +**Common mistake**: Getting controllers before joining. ALWAYS wait for the `MEETING_STATUS_INMEETING` callback! + +--- + +## Troubleshooting Steps + +### Step 1: Check JWT Token + +```cpp +std::cout << "JWT length: " << jwt_token.length() << std::endl; +std::cout << "JWT preview: " << jwt_token.substr(0, 30) << "..." << std::endl; +``` + +Expected output: +``` +JWT length: 358 +JWT preview: eyJhbGciOiJIUzI1NiIsInR5cCI... +``` + +### Step 2: Verify SDKAuth Return + +```cpp +SDKError err = g_authService->SDKAuth(authContext); +std::cout << "SDKAuth returned: " << err << std::endl; +``` + +- `0` (SDKERR_SUCCESS): Good, wait for callback +- Non-zero: Immediate error, check JWT format + +### Step 3: Confirm Callback Fires + +Add logging in `onAuthenticationReturn`: +```cpp +void onAuthenticationReturn(AuthResult ret) override { + std::cout << "*** CALLBACK RECEIVED ***" << std::endl; // First line! + // ... rest of code ... +} +``` + +If you never see "CALLBACK RECEIVED", you need a message loop! + +### Step 4: Test Network + +```cpp +// Before SDKAuth +std::cout << "Testing network..." << std::endl; +// Try to access zoom.us or check internet connection +``` + +--- + +## See Also + +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Why callbacks don't fire +- [Build Errors](../troubleshooting/build-errors.md) - Compilation issues +- [JWT Token Generation](../../../oauth/SKILL.md) - Creating JWT tokens +- [Joining Meetings](../SKILL.md) - Next steps after authentication diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/breakout-rooms.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/breakout-rooms.md new file mode 100644 index 00000000..848d04e6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/breakout-rooms.md @@ -0,0 +1,552 @@ +# Breakout Rooms Example + +## Overview + +This guide shows how to manage breakout rooms in the Zoom Windows Meeting SDK. Breakout rooms functionality works with **both Default UI and Custom UI**. + +--- + +## Understanding Breakout Room Roles + +Breakout room functionality is controlled by **five distinct roles**. A user can have multiple roles simultaneously. + +| Role | Interface | Capabilities | +|------|-----------|--------------| +| **Data** | `IBOData` | Read breakout room info, user assignments, names | +| **Admin** | `IBOAdmin` | Manage running BOs, receive help requests, assign users, broadcast | +| **Creator** | `IBOCreator` | Create/modify BOs, configure settings, preassign users | +| **Assistant** | `IBOAssistant` | Join any BO without assignment (minor role) | +| **Attendee** | `IBOAttendee` | Join assigned BO, request help from admin | + +**Typical role combinations**: +- **Host**: Creator + Admin + Data +- **Co-host**: Admin + Data +- **Regular participant**: Attendee + Data + +--- + +## Step 1: Set Up Role Listener + +First, implement `IMeetingBOControllerEvent` to receive role assignment callbacks: + +**BOControllerEventListener.h**: +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class BOControllerEventListener : public IMeetingBOControllerEvent { +public: + // Role assignment callbacks - store interface pointers when received + void onHasCreatorRightsNotification(IBOCreator* pCreatorObj) override { + std::cout << "[BO] Received CREATOR role" << std::endl; + m_boCreator = pCreatorObj; + } + + void onHasAdminRightsNotification(IBOAdmin* pAdminObj) override { + std::cout << "[BO] Received ADMIN role" << std::endl; + m_boAdmin = pAdminObj; + } + + void onHasAttendeeRightsNotification(IBOAttendee* pAttendeeObj) override { + std::cout << "[BO] Received ATTENDEE role" << std::endl; + m_boAttendee = pAttendeeObj; + } + + void onHasDataHelperRightsNotification(IBOData* pDataHelperObj) override { + std::cout << "[BO] Received DATA role" << std::endl; + m_boData = pDataHelperObj; + } + + void onHasAssistantRightsNotification(IBOAssistant* pAssistantObj) override { + std::cout << "[BO] Received ASSISTANT role" << std::endl; + m_boAssistant = pAssistantObj; + } + + // Role removal callbacks - clear interface pointers + void onLostCreatorRightsNotification() override { + std::cout << "[BO] Lost CREATOR role" << std::endl; + m_boCreator = nullptr; + } + + void onLostAdminRightsNotification() override { + std::cout << "[BO] Lost ADMIN role" << std::endl; + m_boAdmin = nullptr; + } + + void onLostAttendeeRightsNotification() override { + std::cout << "[BO] Lost ATTENDEE role" << std::endl; + m_boAttendee = nullptr; + } + + void onLostDataHelperRightsNotification() override { + std::cout << "[BO] Lost DATA role" << std::endl; + m_boData = nullptr; + } + + void onLostAssistantRightsNotification() override { + std::cout << "[BO] Lost ASSISTANT role" << std::endl; + m_boAssistant = nullptr; + } + + // BO status changes + void onNewBroadcastMessageReceived(const zchar_t* strMsg, unsigned int nSenderID) override { + std::wcout << L"[BO] Broadcast message: " << strMsg << std::endl; + } + + void onBOStopCountDown(unsigned int nSeconds) override { + std::cout << "[BO] Rooms closing in " << nSeconds << " seconds" << std::endl; + } + + void onHostInviteReturnToMainSession(const zchar_t* strName, IReturnToMainSessionHandler* pHandler) override { + std::wcout << L"[BO] Host inviting back to main session from: " << strName << std::endl; + } + + void onBOStatusChanged(BO_STATUS eStatus) override { + std::cout << "[BO] Status changed: " << eStatus << std::endl; + } + + // Accessors for stored interfaces + IBOCreator* GetCreator() { return m_boCreator; } + IBOAdmin* GetAdmin() { return m_boAdmin; } + IBOAttendee* GetAttendee() { return m_boAttendee; } + IBOData* GetData() { return m_boData; } + IBOAssistant* GetAssistant() { return m_boAssistant; } + +private: + IBOCreator* m_boCreator = nullptr; + IBOAdmin* m_boAdmin = nullptr; + IBOAttendee* m_boAttendee = nullptr; + IBOData* m_boData = nullptr; + IBOAssistant* m_boAssistant = nullptr; +}; +``` + +**Register the listener**: +```cpp +void SetupBreakoutRoomListener() { + IMeetingBOController* boController = meetingService->GetMeetingBOController(); + if (!boController) { + std::cerr << "[BO] ERROR: Failed to get BO controller" << std::endl; + return; + } + + BOControllerEventListener* boListener = new BOControllerEventListener(); + boController->SetEvent(boListener); + + std::cout << "[BO] Breakout room listener registered" << std::endl; +} +``` + +--- + +## Step 2: Creator Role - Create & Configure Breakout Rooms + +If you're the host, you'll receive the Creator role and can create rooms: + +```cpp +void CreateBreakoutRooms(IBOCreator* creator) { + if (!creator) { + std::cerr << "[BO] ERROR: No creator role" << std::endl; + return; + } + + // Option A: Create single room + bool success = creator->CreateBO(L"Discussion Room 1"); + std::cout << "[BO] Create room 1: " << (success ? "OK" : "FAILED") << std::endl; + + success = creator->CreateBO(L"Discussion Room 2"); + std::cout << "[BO] Create room 2: " << (success ? "OK" : "FAILED") << std::endl; + + // Option B: Batch create rooms + IList* roomNames = /* your list of names */; + success = creator->CreateGroupBO(roomNames); + std::cout << "[BO] Batch create: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Configure breakout room options**: +```cpp +void ConfigureBreakoutRooms(IBOCreator* creator) { + if (!creator) return; + + BOOption option; + + // Timer settings + option.IsBOTimerEnabled = true; + option.timerDuration = 15; // 15 minutes + option.IsTimerAutoStopBOEnabled = true; // Auto-close after timer + + // Countdown before closing + option.countdown = BOStopCountdown_Seconds_60; // 60 second warning + + // Participant permissions + option.IsParticipantCanChooseBO = true; // Participants can self-select room + option.IsParticipantCanReturnToMainSessionAtAnyTime = true; + option.IsAutoMoveAllAssignedParticipantsEnabled = true; + + // For webinars + option.IsPanelistCanChooseBO = true; + option.IsAttendeeCanChooseBO = true; + option.IsAttendeeContained = true; + + // User limits per room + option.IsUserConfigMaxRoomUserLimitsEnabled = true; + option.nUserConfigMaxRoomUserLimits = 10; + + bool success = creator->SetBOOption(option); + std::cout << "[BO] Configure options: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Preassign users to rooms** (before rooms start): +```cpp +void PreassignUser(IBOCreator* creator, IBOData* data, const zchar_t* userName, const zchar_t* roomName) { + if (!creator || !data) return; + + // Find user ID + IList* unassignedUsers = data->GetUnassignedUserList(); + const zchar_t* userId = nullptr; + + for (int i = 0; i < unassignedUsers->GetCount(); i++) { + const zchar_t* uid = unassignedUsers->GetItem(i); + if (wcscmp(data->GetBOUserName(uid), userName) == 0) { + userId = uid; + break; + } + } + + if (!userId) { + std::wcerr << L"[BO] User not found: " << userName << std::endl; + return; + } + + // Find room ID + IList* roomIds = data->GetBOMeetingIDList(); + const zchar_t* roomId = nullptr; + + for (int i = 0; i < roomIds->GetCount(); i++) { + const zchar_t* rid = roomIds->GetItem(i); + IBOMeeting* room = data->GetBOMeetingByID(rid); + if (room && wcscmp(room->GetBOName(), roomName) == 0) { + roomId = rid; + break; + } + } + + if (!roomId) { + std::wcerr << L"[BO] Room not found: " << roomName << std::endl; + return; + } + + // Assign user to room + bool success = creator->AssignUserToBO(userId, roomId); + std::wcout << L"[BO] Assign " << userName << L" to " << roomName << L": " + << (success ? L"OK" : L"FAILED") << std::endl; +} +``` + +--- + +## Step 3: Admin Role - Manage Running Breakout Rooms + +Once rooms are created, the Admin role manages them: + +```cpp +void StartBreakoutRooms(IBOAdmin* admin) { + if (!admin) { + std::cerr << "[BO] ERROR: No admin role" << std::endl; + return; + } + + if (!admin->CanStartBO()) { + std::cerr << "[BO] Cannot start BOs - check if rooms are created and users assigned" << std::endl; + return; + } + + bool success = admin->StartBO(); + std::cout << "[BO] Start rooms: " << (success ? "OK" : "FAILED") << std::endl; +} + +void StopBreakoutRooms(IBOAdmin* admin) { + if (!admin) return; + + bool success = admin->StopBO(); + std::cout << "[BO] Stop rooms: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Assign users to RUNNING rooms**: +```cpp +void AssignUserToRunningRoom(IBOAdmin* admin, const zchar_t* userId, const zchar_t* roomId) { + if (!admin) return; + + bool success = admin->AssignNewUserToRunningBO(userId, roomId); + std::cout << "[BO] Assign to running room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void SwitchUserRoom(IBOAdmin* admin, const zchar_t* userId, const zchar_t* newRoomId) { + if (!admin) return; + + bool success = admin->SwitchAssignedUserToRunningBO(userId, newRoomId); + std::cout << "[BO] Switch user room: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Broadcast message to all rooms**: +```cpp +void BroadcastMessage(IBOAdmin* admin, const wchar_t* message) { + if (!admin) return; + + bool success = admin->BroadcastMessage(message); + std::wcout << L"[BO] Broadcast: " << (success ? L"OK" : L"FAILED") << std::endl; +} +``` + +**Handle help requests**: +```cpp +class BOAdminEventListener : public IBOAdminEvent { +public: + void onHelpRequestReceived(const zchar_t* strUserID) override { + std::cout << "[BO] Help request from user: " << strUserID << std::endl; + + // Option 1: Join their room to help + m_admin->JoinBOByUserRequest(strUserID); + + // Option 2: Ignore the request + // m_admin->IgnoreUserHelpRequest(strUserID); + } + + void onStartBOError(BOControllerError errCode) override { + std::cerr << "[BO] Start error: " << errCode << std::endl; + } + + void onBOEndTimerUpdated(int remaining, bool isTimesUpNotice) override { + std::cout << "[BO] Timer: " << remaining << " seconds remaining" << std::endl; + } + + void SetAdmin(IBOAdmin* admin) { m_admin = admin; } + +private: + IBOAdmin* m_admin = nullptr; +}; +``` + +--- + +## Step 4: Data Role - Read Breakout Room Information + +The Data role lets you read BO information: + +```cpp +void ListBreakoutRooms(IBOData* data) { + if (!data) return; + + IList* roomIds = data->GetBOMeetingIDList(); + if (!roomIds) { + std::cout << "[BO] No breakout rooms" << std::endl; + return; + } + + std::cout << "[BO] Found " << roomIds->GetCount() << " breakout rooms:" << std::endl; + + for (int i = 0; i < roomIds->GetCount(); i++) { + const zchar_t* roomId = roomIds->GetItem(i); + IBOMeeting* room = data->GetBOMeetingByID(roomId); + + if (room) { + std::wcout << L" Room: " << room->GetBOName() << std::endl; + + // List users in this room + IList* users = room->GetBOUserList(); + if (users) { + for (int j = 0; j < users->GetCount(); j++) { + const zchar_t* userId = users->GetItem(j); + BO_USER_STATUS status = room->GetBOUserStatus(userId); + std::wcout << L" - " << data->GetBOUserName(userId) + << L" (status: " << status << L")" << std::endl; + } + } + } + } +} + +void ListUnassignedUsers(IBOData* data) { + if (!data) return; + + IList* unassigned = data->GetUnassignedUserList(); + if (!unassigned || unassigned->GetCount() == 0) { + std::cout << "[BO] No unassigned users" << std::endl; + return; + } + + std::cout << "[BO] Unassigned users:" << std::endl; + for (int i = 0; i < unassigned->GetCount(); i++) { + const zchar_t* userId = unassigned->GetItem(i); + std::wcout << L" - " << data->GetBOUserName(userId); + if (data->IsBOUserMyself(userId)) { + std::wcout << L" (me)"; + } + std::wcout << std::endl; + } +} +``` + +**Listen for data updates**: +```cpp +class BODataEventListener : public IBODataEvent { +public: + void onBOInfoUpdated(const zchar_t* strBOID) override { + std::cout << "[BO] Room info updated: " << strBOID << std::endl; + } + + void OnBOListInfoUpdated() override { + std::cout << "[BO] Room list updated" << std::endl; + } + + void onUnAssignedUserUpdated() override { + std::cout << "[BO] Unassigned user list updated" << std::endl; + } +}; + +// Register: data->SetEvent(new BODataEventListener()); +``` + +--- + +## Step 5: Attendee Role - Join & Leave Breakout Rooms + +As a regular participant: + +```cpp +void JoinMyBreakoutRoom(IBOAttendee* attendee) { + if (!attendee) { + std::cerr << "[BO] ERROR: No attendee role" << std::endl; + return; + } + + // Get your assigned room name + const zchar_t* roomName = attendee->GetBoName(); + std::wcout << L"[BO] Your assigned room: " << roomName << std::endl; + + // Join the room + bool success = attendee->JoinBo(); + std::cout << "[BO] Join room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void LeaveBreakoutRoom(IBOAttendee* attendee) { + if (!attendee) return; + + if (!attendee->IsCanReturnMainSession()) { + std::cerr << "[BO] Cannot return to main session (disabled by host)" << std::endl; + return; + } + + bool success = attendee->LeaveBo(); + std::cout << "[BO] Leave room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void RequestHelpFromHost(IBOAttendee* attendee) { + if (!attendee) return; + + // Only request help if host is NOT already in your room + if (attendee->IsHostInThisBO()) { + std::cout << "[BO] Host is already in this room" << std::endl; + return; + } + + bool success = attendee->RequestForHelp(); + std::cout << "[BO] Help request: " << (success ? "sent" : "FAILED") << std::endl; +} +``` + +--- + +## Step 6: Assistant Role - Join Any Room + +Co-hosts typically get the Assistant role: + +```cpp +void JoinAnyRoom(IBOAssistant* assistant, const zchar_t* roomId) { + if (!assistant) return; + + bool success = assistant->JoinBO(roomId); + std::cout << "[BO] Join room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void LeaveCurrentRoom(IBOAssistant* assistant) { + if (!assistant) return; + + bool success = assistant->LeaveBO(); + std::cout << "[BO] Leave room: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +--- + +## Error Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | `BOControllerError_NULL_POINTER` | BO controller is null - SDK not initialized | +| 1 | `BOControllerError_WRONG_CURRENT_STATUS` | Incorrect current status | +| 2 | `BOControllerError_TOKEN_NOT_READY` | Token is not ready | +| 3 | `BOControllerError_NO_PRIVILEGE` | No privilege to perform action | +| 4 | `BOControllerError_BO_LIST_IS_UPLOADING` | BO list is being uploaded | +| 5 | `BOControllerError_UPLOAD_FAIL` | BO list upload failed | +| 6 | `BOControllerError_NO_ONE_HAS_BEEN_ASSIGNED` | Cannot start - no users assigned | +| 100 | `BOControllerError_UNKNOWN` | Unknown error | + +--- + +## Complete Workflow Example + +```cpp +void OnInMeeting() { + // Set up listener first + SetupBreakoutRoomListener(); + + // Wait for role callbacks... +} + +void OnCreatorRoleReceived(IBOCreator* creator, IBOData* data) { + // Step 1: Configure options + ConfigureBreakoutRooms(creator); + + // Step 2: Create rooms + creator->CreateBO(L"Team Alpha"); + creator->CreateBO(L"Team Beta"); + + // Step 3: Wait for onCreateBOResponse callback... +} + +void OnRoomsCreated(IBOCreator* creator, IBOData* data, IBOAdmin* admin) { + // Step 4: Assign users + PreassignUser(creator, data, L"John", L"Team Alpha"); + PreassignUser(creator, data, L"Jane", L"Team Beta"); + + // Step 5: Start rooms + StartBreakoutRooms(admin); +} + +void OnAttendeeRoleReceived(IBOAttendee* attendee) { + // As a participant, join your assigned room + JoinMyBreakoutRoom(attendee); +} +``` + +--- + +## Related Documentation + +- [Common Issues](../troubleshooting/common-issues.md) - Error code reference +- [Interface Methods](../references/interface-methods.md) - Complete interface reference +- [Custom UI Architecture](../concepts/custom-ui-architecture.md) - How UI works with breakout rooms + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/captions-transcription.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/captions-transcription.md new file mode 100644 index 00000000..159e6ff7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/captions-transcription.md @@ -0,0 +1,551 @@ +# Closed Captions and Live Transcription + +## Overview + +The Meeting SDK provides two related caption features through `IClosedCaptionController`: + +1. **Manual Closed Captions** - Host assigns someone to type captions manually +2. **Live Transcription** - Automatic speech-to-text with optional translation + +Both features support multi-language translation when enabled. + +## Architecture + +``` +IMeetingService + └── GetMeetingClosedCaptionController() → IClosedCaptionController + ├── SetEvent(IClosedCaptionControllerEvent*) + ├── EnableCaptions(bool) + ├── StartLiveTranscription() + ├── SetMeetingSpokenLanguage(int) + ├── SetTranslationLanguage(int) + └── SendClosedCaption(const zchar_t*) +``` + +## Required Headers + +```cpp +#include +#include +``` + +## Step 1: Implement the Caption Event Listener + +```cpp +// ClosedCaptionControllerEventListener.h +#pragma once +#include +#include + +class ClosedCaptionControllerEventListener + : public ZOOMSDK::IClosedCaptionControllerEvent { +public: + using TranscriptionCallback = std::function; + using CaptionCallback = std::function; + + ClosedCaptionControllerEventListener( + TranscriptionCallback onTranscription = nullptr, + CaptionCallback onCaption = nullptr + ); + virtual ~ClosedCaptionControllerEventListener() = default; + + // Called when assigned to send closed captions + virtual void onAssignedToSendCC(bool bAssigned) override; + + // Called when manual closed caption is received + virtual void onClosedCaptionMsgReceived( + const zchar_t* ccMsg, + unsigned int sender_id, + time_t time + ) override; + + // Called when live transcription status changes + virtual void onLiveTranscriptionStatus( + ZOOMSDK::SDKLiveTranscriptionStatus status + ) override; + + // Called when original (untranslated) message is received + virtual void onOriginalLanguageMsgReceived( + ZOOMSDK::ILiveTranscriptionMessageInfo* messageInfo + ) override; + + // Called when translated transcription message is received + virtual void onLiveTranscriptionMsgInfoReceived( + ZOOMSDK::ILiveTranscriptionMessageInfo* messageInfo + ) override; + + // Called when translation error occurs + virtual void onLiveTranscriptionMsgError( + ZOOMSDK::ILiveTranscriptionLanguage* spokenLanguage, + ZOOMSDK::ILiveTranscriptionLanguage* transcriptLanguage + ) override; + + // Host receives this when someone requests transcription + virtual void onRequestForLiveTranscriptReceived( + unsigned int requester_id, + bool bAnonymous + ) override; + + // Called when request status changes + virtual void onRequestLiveTranscriptionStatusChange(bool bEnabled) override; + + // Called when caption enable status changes + virtual void onCaptionStatusChanged(bool bEnabled) override; + + // Called when someone requests to start captions + virtual void onStartCaptionsRequestReceived( + ZOOMSDK::ICCRequestHandler* handler + ) override; + + // Called when caption request is approved + virtual void onStartCaptionsRequestApproved() override; + + // Called when manual caption status changes + virtual void onManualCaptionStatusChanged(bool bEnabled) override; + + // Called when spoken language changes + virtual void onSpokenLanguageChanged( + ZOOMSDK::ILiveTranscriptionLanguage* spokenLanguage + ) override; + +private: + TranscriptionCallback m_onTranscription; + CaptionCallback m_onCaption; + std::string wstringToUtf8(const std::wstring& wstr); +}; +``` + +```cpp +// ClosedCaptionControllerEventListener.cpp +#include "ClosedCaptionControllerEventListener.h" +#include + +using namespace ZOOMSDK; + +ClosedCaptionControllerEventListener::ClosedCaptionControllerEventListener( + TranscriptionCallback onTranscription, + CaptionCallback onCaption +) : m_onTranscription(onTranscription), m_onCaption(onCaption) {} + +void ClosedCaptionControllerEventListener::onAssignedToSendCC(bool bAssigned) { + std::cout << "Assigned to send CC: " << (bAssigned ? "yes" : "no") << std::endl; +} + +void ClosedCaptionControllerEventListener::onClosedCaptionMsgReceived( + const zchar_t* ccMsg, + unsigned int sender_id, + time_t time +) { + if (ccMsg) { + std::wstring wstr(ccMsg); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[CC] " << utf8 << std::endl; + + if (m_onCaption) { + m_onCaption(wstr, sender_id); + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionStatus( + SDKLiveTranscriptionStatus status +) { + switch (status) { + case SDK_LiveTranscription_Status_Stop: + std::cout << "Live transcription: STOPPED" << std::endl; + break; + case SDK_LiveTranscription_Status_Start: + std::cout << "Live transcription: STARTED" << std::endl; + break; + case SDK_LiveTranscription_Status_User_Sub: + std::cout << "Live transcription: USER SUBSCRIBED" << std::endl; + break; + case SDK_LiveTranscription_Status_Connecting: + std::cout << "Live transcription: CONNECTING" << std::endl; + break; + } +} + +void ClosedCaptionControllerEventListener::onOriginalLanguageMsgReceived( + ILiveTranscriptionMessageInfo* messageInfo +) { + if (!messageInfo) return; + + // Only process complete messages (not partial/streaming) + if (messageInfo->GetMessageOperationType() == SDK_LiveTranscription_OperationType_Complete) { + const zchar_t* content = messageInfo->GetMessageContent(); + if (content) { + std::wstring wstr(content); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[Original] " << utf8 << std::endl; + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionMsgInfoReceived( + ILiveTranscriptionMessageInfo* messageInfo +) { + if (!messageInfo) return; + + // Only process complete messages + SDKLiveTranscriptionOperationType opType = messageInfo->GetMessageOperationType(); + if (opType == SDK_LiveTranscription_OperationType_Complete) { + const zchar_t* content = messageInfo->GetMessageContent(); + unsigned int speakerId = messageInfo->GetSpeakerID(); + + if (content) { + std::wstring wstr(content); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[Transcription] " << utf8 << std::endl; + + if (m_onTranscription) { + m_onTranscription(wstr, speakerId); + } + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionMsgError( + ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage +) { + std::cout << "Transcription error occurred" << std::endl; + if (spokenLanguage) { + std::wcout << L" Spoken: " << spokenLanguage->GetLanguageName() << std::endl; + } + if (transcriptLanguage) { + std::wcout << L" Target: " << transcriptLanguage->GetLanguageName() << std::endl; + } +} + +void ClosedCaptionControllerEventListener::onRequestForLiveTranscriptReceived( + unsigned int requester_id, + bool bAnonymous +) { + std::cout << "Live transcript requested by user " + << (bAnonymous ? "(anonymous)" : std::to_string(requester_id)) + << std::endl; +} + +void ClosedCaptionControllerEventListener::onRequestLiveTranscriptionStatusChange(bool bEnabled) { + std::cout << "Request transcription status: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onCaptionStatusChanged(bool bEnabled) { + std::cout << "Caption status: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onStartCaptionsRequestReceived( + ICCRequestHandler* handler +) { + std::cout << "Caption start request received" << std::endl; + // Host can approve or deny here +} + +void ClosedCaptionControllerEventListener::onStartCaptionsRequestApproved() { + std::cout << "Caption start request approved" << std::endl; +} + +void ClosedCaptionControllerEventListener::onManualCaptionStatusChanged(bool bEnabled) { + std::cout << "Manual caption: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onSpokenLanguageChanged( + ILiveTranscriptionLanguage* spokenLanguage +) { + if (spokenLanguage) { + std::wcout << L"Spoken language changed to: " + << spokenLanguage->GetLanguageName() << std::endl; + } +} + +std::string ClosedCaptionControllerEventListener::wstringToUtf8(const std::wstring& wstr) { + std::string utf8Str; + for (wchar_t wchar : wstr) { + if (wchar < 0x80) { + utf8Str += static_cast(wchar); + } else if (wchar < 0x800) { + utf8Str += static_cast(0xC0 | ((wchar >> 6) & 0x1F)); + utf8Str += static_cast(0x80 | (wchar & 0x3F)); + } else { + utf8Str += static_cast(0xE0 | ((wchar >> 12) & 0x0F)); + utf8Str += static_cast(0x80 | ((wchar >> 6) & 0x3F)); + utf8Str += static_cast(0x80 | (wchar & 0x3F)); + } + } + return utf8Str; +} +``` + +## Step 2: Initialize Caption Controller + +```cpp +// Global variables +IClosedCaptionController* g_captionController = nullptr; +ClosedCaptionControllerEventListener* g_captionListener = nullptr; + +void initializeCaptions(IMeetingService* meetingService) { + // Get the caption controller + g_captionController = meetingService->GetMeetingClosedCaptionController(); + if (!g_captionController) { + std::cerr << "Failed to get caption controller" << std::endl; + return; + } + + // Create and set event listener + g_captionListener = new ClosedCaptionControllerEventListener( + // Transcription callback + [](const std::wstring& text, unsigned int speakerId) { + // Process transcription text + std::wcout << L"Speaker " << speakerId << L": " << text << std::endl; + }, + // Caption callback + [](const std::wstring& text, unsigned int senderId) { + // Process manual caption + std::wcout << L"Caption: " << text << std::endl; + } + ); + + SDKError err = g_captionController->SetEvent(g_captionListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set caption listener: " << err << std::endl; + return; + } + + std::cout << "Caption controller initialized" << std::endl; +} +``` + +## Step 3: Enable Live Transcription + +### Check Feature Availability + +```cpp +void checkTranscriptionFeatures() { + if (!g_captionController) return; + + std::cout << "=== Transcription Features ===" << std::endl; + std::cout << "Captions enabled: " + << g_captionController->IsCaptionsEnabled() << std::endl; + std::cout << "Live transcription feature: " + << g_captionController->IsLiveTranscriptionFeatureEnabled() << std::endl; + std::cout << "Manual caption enabled: " + << g_captionController->IsMeetingManualCaptionEnabled() << std::endl; + std::cout << "Multi-language transcription: " + << g_captionController->IsMultiLanguageTranscriptionEnabled() << std::endl; + std::cout << "Receive spoken language: " + << g_captionController->IsReceiveSpokenLanguageContentEnabled() << std::endl; + std::cout << "Request to start enabled: " + << g_captionController->IsRequestToStartLiveTranscriptionEnabled() << std::endl; + std::cout << "Text translation enabled: " + << g_captionController->IsTextLiveTranslationEnabled() << std::endl; +} +``` + +### Start Live Transcription (with Translation) + +```cpp +void startLiveTranscription() { + if (!g_captionController) return; + + SDKError err; + + // Start live transcription service + err = g_captionController->StartLiveTranscription(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to start transcription: " << err << std::endl; + // May need host permission + } + + // Set spoken language (0 = English) + // Use GetAvailableSpokenLanguageList() to get all options + err = g_captionController->SetMeetingSpokenLanguage(0); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set spoken language: " << err << std::endl; + } + + // Set translation language (1 = Chinese, -1 = disable translation) + err = g_captionController->SetTranslationLanguage(1); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set translation language: " << err << std::endl; + } + + // Enable receiving original spoken language content + err = g_captionController->EnableReceiveSpokenLanguageContent(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable spoken language content: " << err << std::endl; + } + + std::cout << "Live transcription started" << std::endl; +} +``` + +### Stop Live Transcription + +```cpp +void stopLiveTranscription() { + if (!g_captionController) return; + + SDKError err = g_captionController->StopLiveTranscription(); + if (err == SDKERR_SUCCESS) { + std::cout << "Live transcription stopped" << std::endl; + } +} +``` + +## Step 4: Manual Closed Captions (Host Only) + +### Enable Manual Captions + +```cpp +void enableManualCaptions() { + if (!g_captionController) return; + + SDKError err; + + // Enable captions feature + err = g_captionController->EnableCaptions(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable captions: " << err << std::endl; + } + + // Enable manual caption mode + err = g_captionController->EnableMeetingManualCaption(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable manual captions: " << err << std::endl; + } + + std::cout << "Manual captions enabled" << std::endl; +} +``` + +### Assign Caption Privilege + +```cpp +void assignCaptionPrivilege(unsigned int userId, bool assign) { + if (!g_captionController) return; + + // Host assigns someone to type captions + // userId = 0 means current user + SDKError err = g_captionController->AssignCCPrivilege(userId, assign); + if (err == SDKERR_SUCCESS) { + std::cout << "Caption privilege " + << (assign ? "assigned to" : "removed from") + << " user " << userId << std::endl; + } else { + std::cerr << "Failed to assign caption privilege: " << err << std::endl; + } +} +``` + +### Send Manual Caption + +```cpp +void sendManualCaption(const wchar_t* captionText) { + if (!g_captionController) return; + + // Only works if you have been assigned CC privilege + SDKError err = g_captionController->SendClosedCaption(captionText); + if (err == SDKERR_SUCCESS) { + std::cout << "Caption sent" << std::endl; + } else if (err == SDKERR_NO_PERMISSION) { + std::cerr << "No permission to send captions" << std::endl; + } +} +``` + +## Complete Integration Example + +```cpp +void onInMeeting(IMeetingService* meetingService) { + initializeCaptions(meetingService); + + // Check available features + checkTranscriptionFeatures(); + + // Start live transcription with translation + startLiveTranscription(); +} + +// For host: enable manual captions +void onIsHost() { + enableManualCaptions(); + assignCaptionPrivilege(0, true); // Assign to self +} +``` + +## Language IDs + +Common language IDs for `SetMeetingSpokenLanguage()` and `SetTranslationLanguage()`: + +| ID | Language | +|----|----------| +| 0 | English | +| 1 | Chinese (Simplified) | +| 2 | Japanese | +| 3 | German | +| 4 | French | +| 5 | Russian | +| 6 | Portuguese | +| 7 | Spanish | +| 8 | Korean | +| -1 | Disable translation | + +Use `GetAvailableSpokenLanguageList()` and `GetAvailableTranslationLanguageList()` to get the full list of supported languages for the meeting. + +## Transcription Operation Types + +```cpp +enum SDKLiveTranscriptionOperationType { + SDK_LiveTranscription_OperationType_None = 0, + SDK_LiveTranscription_OperationType_Add, // New text added + SDK_LiveTranscription_OperationType_Update, // Text updated + SDK_LiveTranscription_OperationType_Delete, // Text deleted + SDK_LiveTranscription_OperationType_Complete, // Sentence complete + SDK_LiveTranscription_OperationType_NotSupported +}; +``` + +**Important**: Only process `SDK_LiveTranscription_OperationType_Complete` for final transcription text. Other types represent partial/streaming results. + +## Transcription Status + +```cpp +enum SDKLiveTranscriptionStatus { + SDK_LiveTranscription_Status_Stop = 0, // Not running + SDK_LiveTranscription_Status_Start = 1, // Running + SDK_LiveTranscription_Status_User_Sub = 2, // User subscribed + SDK_LiveTranscription_Status_Connecting = 10 // Connecting +}; +``` + +## Error Handling + +```cpp +SDKError err = g_captionController->StartLiveTranscription(); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Transcription started" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission (need host or feature disabled)" << std::endl; + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Feature not available in this meeting" << std::endl; + break; + case SDKERR_SERVICE_FAILED: + std::cerr << "Service failed to start" << std::endl; + break; + default: + std::cerr << "Error: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Multi-language transcription**: Must be enabled in meeting settings for translation to work +2. **Manual captions vs transcription**: These are separate features - enable the one you need +3. **Partial results**: Filter by `GetMessageOperationType() == Complete` to avoid streaming updates +4. **Language ID -1**: Setting translation language to -1 receives closed captions without translation +5. **Host permission**: Most enable/disable operations require host privileges +6. **Unicode handling**: Caption text is `zchar_t*` (wide string) - convert to UTF-8 for display diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/chat.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/chat.md new file mode 100644 index 00000000..0648cf45 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/chat.md @@ -0,0 +1,413 @@ +# Meeting Chat - Send and Receive Messages + +## Overview + +The Meeting SDK provides a rich chat API through `IMeetingChatController`. You can: +- Send messages to all participants or specific users +- Receive incoming chat messages +- Apply rich text formatting (bold, italic, links, colors) +- Handle file transfers +- Support threaded conversations + +## Architecture + +``` +IMeetingService + └── GetMeetingChatController() → IMeetingChatController + ├── SetEvent(IMeetingChatCtrlEvent*) + ├── GetChatMessageBuilder() → IChatMsgInfoBuilder + ├── SendChatMsgTo(IChatMsgInfo*) + └── GetChatStatus() +``` + +## Required Headers + +```cpp +#include +#include +``` + +## Step 1: Implement the Chat Event Listener + +```cpp +// MeetingChatEventListener.h +#pragma once +#include + +class MeetingChatEventListener : public ZOOMSDK::IMeetingChatCtrlEvent { +public: + MeetingChatEventListener() = default; + virtual ~MeetingChatEventListener() = default; + + // Called when a new chat message arrives + virtual void onChatMsgNotification( + ZOOMSDK::IChatMsgInfo* chatMsg, + const zchar_t* content + ) override; + + // Called when chat privileges change + virtual void onChatStatusChangedNotification( + ZOOMSDK::ChatStatus* status + ) override; + + // Called when a message is deleted + virtual void onChatMsgDeleteNotification( + const zchar_t* msgID, + ZOOMSDK::SDKChatMessageDeleteType deleteBy + ) override; + + // Called when a message is edited + virtual void onChatMessageEditNotification( + ZOOMSDK::IChatMsgInfo* chatMsg + ) override; + + // Called when meeting chat sharing status changes + virtual void onShareMeetingChatStatusChanged(bool isStart) override; + + // File transfer callbacks + virtual void onFileSendStart(ZOOMSDK::ISDKFileSender* sender) override; + virtual void onFileReceived(ZOOMSDK::ISDKFileReceiver* receiver) override; + virtual void onFileTransferProgress(ZOOMSDK::SDKFileTransferInfo* info) override; +}; +``` + +```cpp +// MeetingChatEventListener.cpp +#include "MeetingChatEventListener.h" +#include + +using namespace ZOOMSDK; + +void MeetingChatEventListener::onChatMsgNotification( + IChatMsgInfo* chatMsg, + const zchar_t* content +) { + if (chatMsg && content) { + // Get sender information + unsigned int senderId = chatMsg->GetSenderUserId(); + const zchar_t* senderName = chatMsg->GetSenderDisplayName(); + + // Get message details + const zchar_t* msgContent = chatMsg->GetContent(); + SDKChatMessageType msgType = chatMsg->GetChatMessageType(); + + std::wcout << L"[Chat] " << senderName << L": " << msgContent << std::endl; + + // Check message type + switch (msgType) { + case SDKChatMessageType_To_All: + std::cout << " (sent to everyone)" << std::endl; + break; + case SDKChatMessageType_To_Individual: + std::cout << " (private message)" << std::endl; + break; + case SDKChatMessageType_To_WaitingRoomUsers: + std::cout << " (to waiting room)" << std::endl; + break; + } + + // Check if this is a threaded reply + const zchar_t* threadId = chatMsg->GetThreadID(); + if (threadId && wcslen(threadId) > 0) { + std::wcout << L" Thread ID: " << threadId << std::endl; + } + } +} + +void MeetingChatEventListener::onChatStatusChangedNotification(ChatStatus* status) { + if (status) { + std::cout << "Chat status changed" << std::endl; + // Check what chat privileges are available + } +} + +void MeetingChatEventListener::onChatMsgDeleteNotification( + const zchar_t* msgID, + SDKChatMessageDeleteType deleteBy +) { + std::wcout << L"Message deleted: " << msgID << std::endl; + switch (deleteBy) { + case SDKChatMessageDeleteType_By_Self: + std::cout << " (deleted by sender)" << std::endl; + break; + case SDKChatMessageDeleteType_By_Host: + std::cout << " (deleted by host)" << std::endl; + break; + case SDKChatMessageDeleteType_By_DLP: + std::cout << " (deleted by DLP policy)" << std::endl; + break; + } +} + +void MeetingChatEventListener::onChatMessageEditNotification(IChatMsgInfo* chatMsg) { + if (chatMsg) { + std::wcout << L"Message edited: " << chatMsg->GetContent() << std::endl; + } +} + +void MeetingChatEventListener::onShareMeetingChatStatusChanged(bool isStart) { + std::cout << "Share meeting chat: " << (isStart ? "started" : "stopped") << std::endl; +} + +void MeetingChatEventListener::onFileSendStart(ISDKFileSender* sender) { + std::cout << "File send started" << std::endl; +} + +void MeetingChatEventListener::onFileReceived(ISDKFileReceiver* receiver) { + std::cout << "File received" << std::endl; +} + +void MeetingChatEventListener::onFileTransferProgress(SDKFileTransferInfo* info) { + // Handle file transfer progress +} +``` + +## Step 2: Initialize Chat Controller + +```cpp +// Global variables +IMeetingChatController* g_chatController = nullptr; +MeetingChatEventListener* g_chatListener = nullptr; + +void initializeChat(IMeetingService* meetingService) { + // Get the chat controller + g_chatController = meetingService->GetMeetingChatController(); + if (!g_chatController) { + std::cerr << "Failed to get chat controller" << std::endl; + return; + } + + // Create and set event listener + g_chatListener = new MeetingChatEventListener(); + SDKError err = g_chatController->SetEvent(g_chatListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set chat event listener: " << err << std::endl; + return; + } + + std::cout << "Chat controller initialized" << std::endl; +} +``` + +## Step 3: Send Chat Messages + +### Simple Text Message (To Everyone) + +```cpp +void sendMessageToAll(const wchar_t* message) { + if (!g_chatController) return; + + // Get the message builder + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) { + std::cerr << "Failed to get message builder" << std::endl; + return; + } + + // Build the message + builder->SetContent(message); + builder->SetReceiver(0); // 0 = everyone + builder->SetMessageType(SDKChatMessageType_To_All); + + // Build and send + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + SDKError err = g_chatController->SendChatMsgTo(msgInfo); + if (err == SDKERR_SUCCESS) { + std::cout << "Message sent successfully" << std::endl; + } else { + std::cerr << "Failed to send message: " << err << std::endl; + } + } +} +``` + +### Private Message (To Specific User) + +```cpp +void sendPrivateMessage(unsigned int userId, const wchar_t* message) { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + builder->SetContent(message); + builder->SetReceiver(userId); // Specific user ID + builder->SetMessageType(SDKChatMessageType_To_Individual); + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + SDKError err = g_chatController->SendChatMsgTo(msgInfo); + if (err == SDKERR_SUCCESS) { + std::cout << "Private message sent" << std::endl; + } + } +} +``` + +### Rich Text Message with Formatting + +```cpp +void sendFormattedMessage() { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + // Set content: "Hello, this is BOLD and this is italic" + const wchar_t* content = L"Hello, this is BOLD and this is italic"; + builder->SetContent(content); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + + // Apply bold to "BOLD" (positions 14-17, 0-indexed) + builder->SetBold(14, 18); + + // Apply italic to "italic" (positions 32-37) + builder->SetItalic(32, 38); + + // Build and send + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +### Message with Link + +```cpp +void sendMessageWithLink() { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + // Set content with link text + const wchar_t* content = L"Check out this website for more info"; + builder->SetContent(content); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + + // Create link attributes + InsertLinkAttrs linkAttrs; + linkAttrs.insertLinkUrl = L"https://zoom.us"; + + // Apply link to "this website" (positions 10-21) + builder->SetInsertLink(linkAttrs, 10, 22); + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +### Threaded Reply + +```cpp +void sendThreadedReply(const wchar_t* threadId, const wchar_t* message) { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + builder->SetContent(message); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + builder->SetThreadId(threadId); // Reply to existing thread + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +## Complete Integration Example + +```cpp +void onInMeeting(IMeetingService* meetingService) { + // Initialize chat when in meeting + initializeChat(meetingService); + + // Send a greeting + sendMessageToAll(L"Hello everyone! Bot has joined the meeting."); +} + +// Call from your meeting status callback +void MeetingServiceEventListener::onMeetingStatusChanged( + MeetingStatus status, + int iResult +) { + if (status == MEETING_STATUS_INMEETING) { + onInMeeting(g_meetingService); + } +} +``` + +## IChatMsgInfoBuilder Methods Reference + +| Method | Description | +|--------|-------------| +| `SetContent(const zchar_t*)` | Set message text content | +| `SetReceiver(unsigned int)` | Set recipient (0 = everyone) | +| `SetMessageType(SDKChatMessageType)` | Set message type (all, individual, waiting room) | +| `SetThreadId(const zchar_t*)` | Set thread ID for replies | +| `SetBold(start, end)` | Apply bold style | +| `SetItalic(start, end)` | Apply italic style | +| `SetUnderline(start, end)` | Apply underline style | +| `SetStrikethrough(start, end)` | Apply strikethrough style | +| `SetFontColor(FontColorAttrs, start, end)` | Set font color | +| `SetBackgroundColor(BackgroundColorAttrs, start, end)` | Set background color | +| `SetFontSize(FontSizeAttrs, start, end)` | Set font size | +| `SetInsertLink(InsertLinkAttrs, start, end)` | Insert hyperlink | +| `SetBulletedList(start, end)` | Apply bulleted list style | +| `SetNumberedList(start, end)` | Apply numbered list style | +| `SetQuotePosition(start, end)` | Apply quote style | +| `SetParagraph(ParagraphAttrs, start, end)` | Set paragraph style (H1, H2, H3) | +| `ClearStyles()` | Clear all styles | +| `Clear()` | Clear all properties | +| `Build()` | Build the IChatMsgInfo object | + +## Chat Message Types + +```cpp +enum SDKChatMessageType { + SDKChatMessageType_To_None, // Invalid + SDKChatMessageType_To_All, // To everyone + SDKChatMessageType_To_Individual, // Private message + SDKChatMessageType_To_Individual_Panelist, // Webinar panelist + SDKChatMessageType_To_WaitingRoomUsers // To waiting room +}; +``` + +## Error Handling + +```cpp +SDKError err = g_chatController->SendChatMsgTo(msgInfo); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Message sent" << std::endl; + break; + case SDKERR_INVALID_PARAMETER: + std::cerr << "Invalid message parameters" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission to send chat" << std::endl; + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Chat not available (not in meeting?)" << std::endl; + break; + default: + std::cerr << "Failed to send: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Chat controller unavailable**: Must be in a meeting (`MEETING_STATUS_INMEETING`) +2. **Position indexing**: Style positions are 0-indexed and use character positions, not byte positions +3. **Unicode support**: Use `wchar_t*` and `std::wstring` for proper Unicode support +4. **Builder reuse**: The builder can be reused, but call `Clear()` between messages +5. **Thread ID**: When replying to a thread, get the thread ID from `IChatMsgInfo::GetThreadID()` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md new file mode 100644 index 00000000..39c7e2fb --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md @@ -0,0 +1,237 @@ +# Custom UI Video Rendering — Complete Working Example + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Examples +> **Prerequisite**: [Custom UI Architecture](../concepts/custom-ui-architecture.md), [Authentication Pattern](authentication-pattern.md) + +## Overview + +This example shows how to create a Custom UI meeting app that: +1. Creates its own Win32 window +2. Uses `ICustomizedVideoContainer` for SDK-rendered video +3. Shows an active speaker element (auto-follows who's talking) +4. Shows gallery elements for each participant +5. Handles screen sharing via `ICustomizedShareRender` + +## Flow + +``` +InitSDK (with ENABLE_CUSTOMIZED_UI_FLAG) + -> AuthSDK (JWT) + -> JoinMeeting + -> OnConnecting: Create window + CustomUIMgr + VideoContainer + -> OnInMeeting: Create video elements + subscribe to participants + -> Message loop (window events + SDK callbacks) + -> OnEnded: Destroy everything +``` + +## Step 1: Enable Custom UI in InitParam + +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +initParam.emLanguageID = LANGUAGE_English; +initParam.enableLogByDefault = true; + +// CRITICAL: This is what makes it Custom UI mode +initParam.obConfigOpts.optionalFeatures = ENABLE_CUSTOMIZED_UI_FLAG; + +SDKError err = InitSDK(initParam); +``` + +## Step 2: Create the Custom UI Manager (on CONNECTING) + +When `onMeetingStatusChanged` fires with `MEETING_STATUS_CONNECTING`, create your window and the Custom UI manager: + +```cpp +#include +#include +#include +#include + +ICustomizedUIMgr* pCustomUIMgr = nullptr; +ICustomizedVideoContainer* pVideoContainer = nullptr; + +// Create the manager (global SDK function) +SDKError err = CreateCustomizedUIMgr(&pCustomUIMgr); + +// Optional: check license (log warning, don't abort) +err = pCustomUIMgr->HasLicense(); +if (err != SDKERR_SUCCESS) { + std::cout << "WARNING: HasLicense returned " << err << std::endl; +} + +// Register for destroy notifications +pCustomUIMgr->SetEvent(&myUIMgrEventListener); + +// Create video container inside your Win32 window +RECT rc; +::GetClientRect(hMyWindow, &rc); +err = pCustomUIMgr->CreateVideoContainer(&pVideoContainer, hMyWindow, rc); + +pVideoContainer->SetEvent(&myVideoContainerEventListener); +pVideoContainer->Show(); +pVideoContainer->SetBkColor(RGB(30, 30, 30)); // Dark background +``` + +## Step 3: Create Video Elements (on IN_MEETING) + +When `onMeetingStatusChanged` fires with `MEETING_STATUS_INMEETING`: + +### Active Speaker Element (auto-follows current speaker) + +```cpp +IVideoRenderElement* pElement = nullptr; +err = pVideoContainer->CreateVideoElement(&pElement, VideoRenderElement_ACTIVE); + +IActiveVideoRenderElement* pActive = dynamic_cast(pElement); +RECT activeRect = { 0, 0, windowWidth, (int)(windowHeight * 0.7) }; +pActive->SetPos(activeRect); +pActive->Show(); +pActive->Start(); // Begin auto-tracking active speaker +``` + +### Normal Elements (specific participants) + +```cpp +IMeetingParticipantsController* pParticipants = + pMeetingService->GetMeetingParticipantsController(); +IList* pUserList = pParticipants->GetParticipantsList(); + +for (int i = 0; i < pUserList->GetCount() && i < MAX_GALLERY; i++) { + unsigned int userId = pUserList->GetItem(i); + + IVideoRenderElement* pNormElement = nullptr; + err = pVideoContainer->CreateVideoElement(&pNormElement, VideoRenderElement_NORMAL); + + INormalVideoRenderElement* pNormal = + dynamic_cast(pNormElement); + + pNormal->Subscribe(userId); + pNormal->SetResolution(VideoRenderResolution_360p); + pNormal->Show(); + + // Position in gallery strip + int elemWidth = windowWidth / galleryCount; + RECT r = { i * elemWidth, galleryTop, (i+1) * elemWidth, windowHeight }; + pNormal->SetPos(r); +} +``` + +## Step 4: Handle Layout + +Respond to `onLayoutNotification` and `WM_SIZE` to re-layout elements: + +```cpp +void LayoutVideoElements() { + RECT clientRect; + ::GetClientRect(hMyWindow, &clientRect); + int totalWidth = clientRect.right - clientRect.left; + int totalHeight = clientRect.bottom - clientRect.top; + + // Resize container to fill window + pVideoContainer->Resize(clientRect); + + if (galleryElements.empty()) { + // Active speaker only — full window + RECT activeRect = { 0, 0, totalWidth, totalHeight }; + pActiveElement->SetPos(activeRect); + } else { + // Active speaker: top 70%, gallery: bottom 30% + int activeHeight = (int)(totalHeight * 0.7); + RECT activeRect = { 0, 0, totalWidth, activeHeight }; + pActiveElement->SetPos(activeRect); + + int elemWidth = totalWidth / (int)galleryElements.size(); + for (int i = 0; i < galleryElements.size(); i++) { + RECT r = { i * elemWidth, activeHeight, (i+1) * elemWidth, totalHeight }; + galleryElements[i]->SetPos(r); + } + } +} +``` + +## Step 5: Handle Screen Sharing + +```cpp +// Create share render (hidden until someone shares) +ICustomizedShareRender* pShareRender = nullptr; +RECT rc; +::GetClientRect(hMyWindow, &rc); +pCustomUIMgr->CreateShareRender(&pShareRender, hMyWindow, rc); +pShareRender->SetEvent(&myShareEventListener); +pShareRender->Hide(); + +// In ShareRenderEventListener: +void onSharingSourceNotification(unsigned int nShareSourceID) { + if (nShareSourceID > 0) { + pShareRender->SetShareSourceID(nShareSourceID); + pShareRender->SetViewMode(CSM_FULLFILL); + pShareRender->Show(); + } else { + pShareRender->Hide(); + } +} +``` + +## Step 6: Cleanup (on meeting end) + +```cpp +void Cleanup() { + if (pVideoContainer) { + pVideoContainer->DestroyAllVideoElement(); + pCustomUIMgr->DestroyVideoContainer(pVideoContainer); + pVideoContainer = nullptr; + } + if (pShareRender) { + pCustomUIMgr->DestroyShareRender(pShareRender); + pShareRender = nullptr; + } + if (pCustomUIMgr) { + DestroyCustomizedUIMgr(pCustomUIMgr); + pCustomUIMgr = nullptr; + } + if (hMyWindow) { + DestroyWindow(hMyWindow); + hMyWindow = nullptr; + } +} +``` + +## Complete Event Listener Implementations + +See [Interface Methods Reference](../references/interface-methods.md) for the full list of required virtual methods for: +- `ICustomizedUIMgrEvent` (3 methods) +- `ICustomizedVideoContainerEvent` (6 methods) +- `ICustomizedShareRenderEvent` (3 methods) + +## Required SDK Headers + +```cpp +#include +#include +#include +#include // CreateCustomizedUIMgr() +#include // ICustomizedUIMgr, ICustomizedUIMgrEvent +#include // ICustomizedVideoContainer, elements +#include // ICustomizedShareRender +#include +#include // Before participants! +#include +``` + +## Key Gotchas + +1. **Create Custom UI on CONNECTING, not IN_MEETING** — the SDK needs the video container ready before it starts rendering +2. **Active element needs `Start()`** — `Show()` alone is not enough, you must call `Start()` to begin active speaker tracking +3. **Normal elements need `Subscribe(userId)`** — without this they show nothing +4. **`SetPos()` coordinates are relative to container**, not screen or parent window +5. **`Resize()` the container when window resizes** — or the D3D surface won't match the window +6. **Destroy order matters** — elements first, then container, then manager + +--- + +**See also:** +- [Custom UI Architecture](../concepts/custom-ui-architecture.md) — How rendering works internally +- [Two Approaches: SDK-Rendered vs Self-Rendered](../concepts/custom-ui-vs-raw-data.md) +- [Raw Video Capture](raw-video-capture.md) — For self-rendered approach diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/local-recording.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/local-recording.md new file mode 100644 index 00000000..45ebc133 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/local-recording.md @@ -0,0 +1,580 @@ +# Local Recording + +## Overview + +The Meeting SDK provides local recording capabilities through `IMeetingRecordingController`. Local recording saves meeting video/audio to the local disk as MP4 files. This is different from: + +- **Cloud Recording** - Saved to Zoom's cloud storage +- **Raw Recording** - Direct access to raw frames (see [raw-video-capture.md](raw-video-capture.md)) + +## Architecture + +``` +IMeetingService + └── GetMeetingRecordingController() → IMeetingRecordingController + ├── SetEvent(IMeetingRecordingCtrlEvent*) + ├── CanStartRecording(bool, unsigned int) + ├── StartRecording(time_t&) + ├── StopRecording() + ├── RequestLocalRecordingPrivilege() + └── IsSupportLocalRecording() +``` + +## Required Headers + +```cpp +#include +#include +#include +``` + +## Step 1: Implement the Recording Event Listener + +```cpp +// MeetingRecordingCtrlEventListener.h +#pragma once +#include +#include + +class MeetingRecordingCtrlEventListener + : public ZOOMSDK::IMeetingRecordingCtrlEvent { +public: + using PermissionCallback = std::function; + + MeetingRecordingCtrlEventListener(PermissionCallback onPermissionGranted = nullptr); + virtual ~MeetingRecordingCtrlEventListener() = default; + + // Local recording status changes + virtual void onRecordingStatus(ZOOMSDK::RecordingStatus status) override; + + // Cloud recording status changes + virtual void onCloudRecordingStatus(ZOOMSDK::RecordingStatus status) override; + + // Recording privilege changed (can now record or lost privilege) + virtual void onRecordPrivilegeChanged(bool bCanRec) override; + + // Result of RequestLocalRecordingPrivilege() + virtual void onLocalRecordingPrivilegeRequestStatus( + ZOOMSDK::RequestLocalRecordingStatus status + ) override; + + // Cloud recording permission request response + virtual void onRequestCloudRecordingResponse( + ZOOMSDK::RequestStartCloudRecordingStatus status + ) override; + + // Host received a recording privilege request + virtual void onLocalRecordingPrivilegeRequested( + ZOOMSDK::IRequestLocalRecordingPrivilegeHandler* handler + ) override; + + // Host received a cloud recording request + virtual void onStartCloudRecordingRequested( + ZOOMSDK::IRequestStartCloudRecordingHandler* handler + ) override; + + // MP4 conversion completed + virtual void onRecording2MP4Done( + bool bsuccess, + int iResult, + const zchar_t* szPath + ) override; + + // MP4 conversion progress + virtual void onRecording2MP4Processing(int iPercentage) override; + + // Custom UI recording layout callback + virtual void onCustomizedLocalRecordingSourceNotification( + ZOOMSDK::ICustomizedLocalRecordingLayoutHelper* layout_helper + ) override; + + // Cloud storage full warning + virtual void onCloudRecordingStorageFull(time_t gracePeriodDate) override; + + // Smart recording request + virtual void onEnableAndStartSmartRecordingRequested( + ZOOMSDK::IRequestEnableAndStartSmartRecordingHandler* handler + ) override; + + // Smart recording enable action + virtual void onSmartRecordingEnableActionCallback( + ZOOMSDK::ISmartRecordingEnableActionHandler* handler + ) override; + +private: + PermissionCallback m_onPermissionGranted; +}; +``` + +```cpp +// MeetingRecordingCtrlEventListener.cpp +#include "MeetingRecordingCtrlEventListener.h" +#include + +using namespace ZOOMSDK; + +MeetingRecordingCtrlEventListener::MeetingRecordingCtrlEventListener( + PermissionCallback onPermissionGranted +) : m_onPermissionGranted(onPermissionGranted) {} + +void MeetingRecordingCtrlEventListener::onRecordingStatus(RecordingStatus status) { + switch (status) { + case Recording_Start: + std::cout << "Recording started" << std::endl; + break; + case Recording_Stop: + std::cout << "Recording stopped" << std::endl; + break; + case Recording_Pause: + std::cout << "Recording paused" << std::endl; + break; + case Recording_Connecting: + std::cout << "Recording connecting..." << std::endl; + break; + case Recording_DiskFull: + std::cerr << "Recording stopped - disk full!" << std::endl; + break; + default: + std::cout << "Recording status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStatus(RecordingStatus status) { + std::cout << "Cloud recording status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecordPrivilegeChanged(bool bCanRec) { + std::cout << "Recording privilege: " << (bCanRec ? "GRANTED" : "REVOKED") << std::endl; + if (bCanRec && m_onPermissionGranted) { + m_onPermissionGranted(); + } +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequestStatus( + RequestLocalRecordingStatus status +) { + switch (status) { + case LocalRecordingRequestStatus_Granted: + std::cout << "Recording privilege request: GRANTED" << std::endl; + break; + case LocalRecordingRequestStatus_Denied: + std::cout << "Recording privilege request: DENIED" << std::endl; + break; + case LocalRecordingRequestStatus_Timeout: + std::cout << "Recording privilege request: TIMEOUT" << std::endl; + break; + default: + std::cout << "Recording privilege request status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRequestCloudRecordingResponse( + RequestStartCloudRecordingStatus status +) { + std::cout << "Cloud recording request response: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequested( + IRequestLocalRecordingPrivilegeHandler* handler +) { + if (handler) { + std::cout << "Recording privilege requested by user: " + << handler->GetRequesterId() << std::endl; + + // Auto-approve (or implement your logic) + // handler->GrantLocalRecordingPrivilege(); + // handler->DenyLocalRecordingPrivilege(); + } +} + +void MeetingRecordingCtrlEventListener::onStartCloudRecordingRequested( + IRequestStartCloudRecordingHandler* handler +) { + std::cout << "Cloud recording start requested" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecording2MP4Done( + bool bsuccess, + int iResult, + const zchar_t* szPath +) { + if (bsuccess) { + std::wcout << L"Recording saved to: " << szPath << std::endl; + } else { + std::cerr << "Recording conversion failed with error: " << iResult << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRecording2MP4Processing(int iPercentage) { + std::cout << "Converting to MP4: " << iPercentage << "%" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCustomizedLocalRecordingSourceNotification( + ICustomizedLocalRecordingLayoutHelper* layout_helper +) { + // Used for custom UI recording layout +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStorageFull(time_t gracePeriodDate) { + std::cerr << "Cloud recording storage full!" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onEnableAndStartSmartRecordingRequested( + IRequestEnableAndStartSmartRecordingHandler* handler +) { + // Smart recording feature request +} + +void MeetingRecordingCtrlEventListener::onSmartRecordingEnableActionCallback( + ISmartRecordingEnableActionHandler* handler +) { + // Smart recording enable action +} +``` + +## Step 2: Initialize Recording Controller + +```cpp +// Global variables +IMeetingRecordingController* g_recordController = nullptr; +MeetingRecordingCtrlEventListener* g_recordListener = nullptr; +IMeetingParticipantsController* g_participantsController = nullptr; + +void initializeRecording(IMeetingService* meetingService) { + // Get recording controller + g_recordController = meetingService->GetMeetingRecordingController(); + if (!g_recordController) { + std::cerr << "Failed to get recording controller" << std::endl; + return; + } + + // Get participants controller (needed for permission checks) + g_participantsController = meetingService->GetMeetingParticipantsController(); + + // Create and set event listener + g_recordListener = new MeetingRecordingCtrlEventListener( + []() { + // Called when recording privilege is granted + attemptToStartRecording(); + } + ); + + SDKError err = g_recordController->SetEvent(g_recordListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set recording listener: " << err << std::endl; + } + + std::cout << "Recording controller initialized" << std::endl; +} +``` + +## Step 3: Check Recording Permission + +```cpp +bool canStartRecording() { + if (!g_recordController) return false; + + // Check if local recording is supported + if (!g_recordController->IsSupportLocalRecording()) { + std::cout << "Local recording not supported" << std::endl; + return false; + } + + // Check if we can start recording + // false = local recording, 0 = current user + SDKError err = g_recordController->CanStartRecording(false, 0); + + if (err == SDKERR_SUCCESS) { + std::cout << "Can start recording" << std::endl; + return true; + } else if (err == SDKERR_NO_PERMISSION) { + std::cout << "No recording permission - requesting..." << std::endl; + + // Request permission from host + g_recordController->RequestLocalRecordingPrivilege(); + return false; + } else { + std::cerr << "Cannot start recording: " << err << std::endl; + return false; + } +} +``` + +## Step 4: Start/Stop Recording + +### Start Local Recording + +```cpp +void attemptToStartRecording() { + if (!g_recordController) return; + + if (!canStartRecording()) { + return; // Permission request will trigger callback when granted + } + + time_t startTime; + SDKError err = g_recordController->StartRecording(startTime); + + if (err == SDKERR_SUCCESS) { + std::cout << "Recording started at: " << startTime << std::endl; + } else { + std::cerr << "Failed to start recording: " << err << std::endl; + } +} +``` + +### Stop Recording + +```cpp +void stopRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->StopRecording(); + + if (err == SDKERR_SUCCESS) { + std::cout << "Recording stopped" << std::endl; + // Wait for onRecording2MP4Done callback for final file path + } else { + std::cerr << "Failed to stop recording: " << err << std::endl; + } +} +``` + +### Pause/Resume Recording + +```cpp +void pauseRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->PauseRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Recording paused" << std::endl; + } +} + +void resumeRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->ResumeRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Recording resumed" << std::endl; + } +} +``` + +## Step 5: Handle Permission Flow + +When you're not the host, you need to request recording permission: + +```cpp +void onInMeeting(IMeetingService* meetingService) { + initializeRecording(meetingService); + + // Check if we're the host + IUserInfo* myInfo = g_participantsController->GetMySelfUser(); + if (myInfo && myInfo->IsHost()) { + std::cout << "We are host - can record directly" << std::endl; + attemptToStartRecording(); + } else { + std::cout << "Not host - need to request permission" << std::endl; + if (!canStartRecording()) { + // Wait for onRecordPrivilegeChanged callback + } + } +} + +void onIsHost() { + std::cout << "Now host - can start recording" << std::endl; + attemptToStartRecording(); +} + +void onIsCoHost() { + std::cout << "Now co-host - checking recording permission" << std::endl; + attemptToStartRecording(); +} +``` + +## Step 6: Monitor Encoder Process (zTscoder.exe) + +After `StopRecording()`, Zoom uses `zTscoder.exe` to convert the recording to MP4. You can monitor this process: + +```cpp +#include +#include + +bool encoderHasStarted = false; +bool encoderFinished = false; + +bool IsProcessRunning(const std::wstring& processName) { + PROCESSENTRY32 entry; + entry.dwSize = sizeof(entry); + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) return false; + + if (!Process32First(snapshot, &entry)) { + CloseHandle(snapshot); + return false; + } + + do { + if (std::wstring(entry.szExeFile) == processName) { + CloseHandle(snapshot); + return true; + } + } while (Process32Next(snapshot, &entry)); + + CloseHandle(snapshot); + return false; +} + +void monitorEncoder() { + const std::wstring encoderProcess = L"zTscoder.exe"; + + // Check if encoder started + if (IsProcessRunning(encoderProcess)) { + encoderHasStarted = true; + std::cout << "Encoder is running..." << std::endl; + } + + // Check if encoder finished + if (encoderHasStarted && !IsProcessRunning(encoderProcess)) { + encoderFinished = true; + std::cout << "Encoder finished - recording ready" << std::endl; + + // Now you can upload/move the recording file + uploadRecording(); + } +} +``` + +## Complete Integration Example + +```cpp +// Message loop with encoder monitoring +int main() { + LoadConfig(); + InitSDK(); + + MSG msg; + const std::wstring encoderProcess = L"zTscoder.exe"; + + while (!g_exit && GetMessage(&msg, nullptr, 0, 0) != 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + + // Monitor encoder after recording stops + if (IsProcessRunning(encoderProcess)) { + encoderHasStarted = true; + } + } + + // Wait for encoder to finish before cleanup + while (encoderHasStarted && !encoderFinished) { + if (!IsProcessRunning(encoderProcess)) { + encoderFinished = true; + std::cout << "Recording conversion complete" << std::endl; + + // Upload or process the recording file + processRecordingFile(); + } + Sleep(1000); // Check every second + } + + // Cleanup + if (meetingService) DestroyMeetingService(meetingService); + if (authService) DestroyAuthService(authService); + CleanUPSDK(); + + return 0; +} +``` + +## Recording Status Values + +```cpp +enum RecordingStatus { + Recording_Start, // Recording started + Recording_Stop, // Recording stopped + Recording_DiskFull, // Disk full error + Recording_Pause, // Recording paused + Recording_Connecting // Connecting to recording service +}; +``` + +## Permission Request Status Values + +```cpp +enum RequestLocalRecordingStatus { + LocalRecordingRequestStatus_Granted, // Request approved + LocalRecordingRequestStatus_Denied, // Request denied + LocalRecordingRequestStatus_Timeout, // Request timed out + LocalRecordingRequestStatus_None // No status +}; +``` + +## Recording File Location + +Local recordings are saved to the user's Zoom recordings folder: +- Default: `%USERPROFILE%\Documents\Zoom\` +- Meeting subfolder: `Meeting ID - Date/` + +The exact path is provided in `onRecording2MP4Done()` callback. + +## Error Handling + +```cpp +SDKError err = g_recordController->StartRecording(startTime); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Recording started" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission to record" << std::endl; + g_recordController->RequestLocalRecordingPrivilege(); + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Cannot record now (meeting not started?)" << std::endl; + break; + case SDKERR_SERVICE_FAILED: + std::cerr << "Recording service failed" << std::endl; + break; + case SDKERR_NO_RECORDING_IN_PROGRESS: + std::cerr << "No recording in progress" << std::endl; + break; + default: + std::cerr << "Error: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Permission timing**: Must be in meeting (`MEETING_STATUS_INMEETING`) before checking permission +2. **Host vs participant**: Non-hosts need to request permission first +3. **Encoder wait**: After `StopRecording()`, must wait for `zTscoder.exe` to finish +4. **MP4 callback**: `onRecording2MP4Done()` requires `EnableLocalRecordingConvertProgressBarDialog(false)` before meeting starts +5. **Disk space**: Recording will stop if disk is full (monitor `Recording_DiskFull`) +6. **View mode**: For gallery view recording, call `SwitchToVideoWall()` before starting + +## Gallery View Recording + +To record gallery view instead of active speaker: + +```cpp +void switchToGalleryView() { + IMeetingUIController* uiController = g_meetingService->GetUIController(); + if (uiController) { + SDKError err = uiController->SwitchToVideoWall(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to switch to gallery view" << std::endl; + } + } +} + +void switchToActiveSpeaker() { + IMeetingUIController* uiController = g_meetingService->GetUIController(); + if (uiController) { + uiController->SwitchToActiveSpeaker(); + } +} +``` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/raw-video-capture.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/raw-video-capture.md new file mode 100644 index 00000000..456f0cab --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/raw-video-capture.md @@ -0,0 +1,814 @@ +# Raw Video Capture Example + +## Overview + +This guide shows how to capture raw video data from Zoom meetings using the Windows Meeting SDK. Raw video data is provided in **YUV420 (I420) format** for video and **PCM format** for audio. + +--- + +## Raw Recording vs Raw Streaming + +There are **two ways** to access raw data in the Zoom SDK: + +| Method | Permission Source | How to Get Permission | Use Case | +|--------|-------------------|----------------------|----------| +| **Raw Recording** | Local recording permission | Host/co-host OR request from host | Capture data with "recording" consent dialog | +| **Raw Streaming** | Live streaming permission | Must be licensed Pro/Business/Education/Enterprise | Capture data with "streaming" consent dialog | + +**Key differences**: +- **Raw Recording**: Disables local recording (`.mp4`) for the SDK user. Host can still cloud record. +- **Raw Streaming**: Other participants see "live streaming" notification instead of "recording". +- Both give you the same raw data access (YUV420 video, PCM audio). + +**SDK Version Requirements**: +- Raw recording: SDK 5.9.0+ +- Raw streaming by host: SDK 5.11.0+ +- Raw streaming by non-host: SDK 5.12.8+ +- Request local recording permission: SDK 5.13.5+ + +--- + +## Permission Requirements + +### For Raw Recording + +The meeting must have **local recording enabled**, AND you must meet **one** of: +- You are the meeting host or co-host +- You have been granted local recording permission by the host +- You joined with an OAuth app privilege token (see below) + +### For Raw Streaming + +You must meet **all** of: +- Current user has raw live streaming permission +- Meeting host has a Pro, Business, Education, or Enterprise account +- Meeting host is licensed for live streaming + +### OAuth App Privilege Token (Advanced) + +You can skip the "request permission from host" step by using OAuth tokens: + +**For recording**: +1. OAuth app requests `Meeting_token:read:admin:local_recording` (admin) or `Meeting_token:read:local_recording` (user) +2. Call REST API: `GET /meetings/{meetingId}/jointoken/local_recording` +3. Pass token to SDK via `app_privilege_token` join parameter + +**For streaming**: +1. OAuth app requests `Meeting_token:read:admin:live_streaming` (admin) or `Meeting_token:read:live_streaming` (user) +2. Call REST API: `GET /meetings/{meetingId}/jointoken/live_streaming` +3. Pass token to SDK via `app_privilege_token` join parameter + +--- + +## Complete Workflow + +### Step 1: Join Meeting Successfully + +Before capturing video, you must: +1. ✅ Initialize SDK +2. ✅ Authenticate with JWT token +3. ✅ Join meeting successfully (`MEETING_STATUS_INMEETING`) + +See [Authentication Pattern](authentication-pattern.md) for this setup. + +--- + +### Step 2: Start Raw Recording (or Streaming) + +**CRITICAL**: You MUST call `StartRawRecording()` before you can capture video data. + +```cpp +void OnInMeeting() { + std::cout << "[VIDEO] In meeting! Starting video capture..." << std::endl; + + // Get recording controller + IMeetingRecordingController* recordingCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordingCtrl) { + std::cerr << "[VIDEO] ERROR: Failed to get recording controller" << std::endl; + return; + } + + // Check if we can start recording + SDKError canStart = recordingCtrl->CanStartRecording(false, 0); + if (canStart != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] Cannot start recording: " << canStart << std::endl; + return; + } + + // Start raw recording (this enables raw data capture) + SDKError err = recordingCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] StartRawRecording failed: " << err << std::endl; + return; + } + + std::cout << "[VIDEO] Raw recording started!" << std::endl; + + // Wait a moment for recording to initialize + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // Continue to Step 3... +} +``` + +**Important notes**: +- `StartRawRecording()` does NOT create a recording file on disk +- It only enables raw data capture via SDK callbacks +- You need host/co-host permissions OR special SDK app privileges +- If you get `SDKERR_WRONG_USAGE`, you may lack permissions + +**Alternative: Use Raw Streaming instead**: +```cpp +// If you have streaming permission instead of recording permission +IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController(); + +SDKError err = streamCtrl->StartRawLiveStream(); +if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] StartRawLiveStream failed: " << err << std::endl; + return; +} + +std::cout << "[VIDEO] Raw streaming started!" << std::endl; +// Same subscription process applies after this +``` + +**Requesting permission at runtime**: +```cpp +// If you're not the host, request permission +IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController(); +streamCtrl->RequestRawLiveStream(L"My AI Bot", L"https://example.com/description"); + +// Listen for onRawLiveStreamPrivilegeChanged callback to know when granted +``` + +--- + +### Step 3: Get Participant IDs + +You need to know which user's video you want to capture: + +```cpp +// Get participants controller +IMeetingParticipantsController* participantsCtrl = + meetingService->GetMeetingParticipantsController(); + +if (!participantsCtrl) { + std::cerr << "[VIDEO] Failed to get participants controller" << std::endl; + return; +} + +// Get list of all participants +IList* participantList = participantsCtrl->GetParticipantsList(); + +if (!participantList || participantList->GetCount() == 0) { + std::cerr << "[VIDEO] No participants in meeting" << std::endl; + return; +} + +// Get first participant's user ID +uint32_t userId = participantList->GetItem(0); +std::cout << "[VIDEO] Found participant: " << userId << std::endl; + +// You can also get the user's name: +IUserInfo* userInfo = participantsCtrl->GetUserByUserID(userId); +if (userInfo) { + const wchar_t* userName = userInfo->GetUserName(); + std::wcout << L"[VIDEO] User name: " << userName << std::endl; +} +``` + +**How to capture specific users**: +- **All participants**: Loop through `participantList` and subscribe to each +- **Active speaker**: Use `IMeetingVideoController()->GetActiveVideoUserID()` +- **Yourself**: Use `IMeetingParticipantsController()->GetMySelfUser()->GetUserID()` +- **Specific name**: Loop and match `GetUserName()` + +--- + +### Step 4: Implement Renderer Delegate + +This class receives the video frames: + +**ZoomSDKRendererDelegate.h**: +```cpp +#pragma once +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + ZoomSDKRendererDelegate(); + ~ZoomSDKRendererDelegate(); + + // Called when a new video frame arrives + void onRawDataFrameReceived(YUVRawDataI420* data) override; + + // Called when raw data status changes + void onRawDataStatusChanged(RawDataStatus status) override; + + // Called before renderer is destroyed + void onRendererBeDestroyed() override; + +private: + void SaveToRawYUVFile(YUVRawDataI420* data); + int frameCount; +}; +``` + +**ZoomSDKRendererDelegate.cpp**: +```cpp +#include "ZoomSDKRendererDelegate.h" + +ZoomSDKRendererDelegate::ZoomSDKRendererDelegate() : frameCount(0) { + std::cout << "[VIDEO] Renderer delegate created" << std::endl; +} + +ZoomSDKRendererDelegate::~ZoomSDKRendererDelegate() { + std::cout << "[VIDEO] Renderer destroyed. Total frames: " << frameCount << std::endl; +} + +void ZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + // Get frame dimensions + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Get rotation (0, 90, 180, 270 degrees) + int rotation = data->GetRotation(); + + // Log every 30 frames (every ~1 second at 30fps) + if (frameCount % 30 == 0) { + std::cout << "[VIDEO] Frame " << frameCount + << " - " << width << "x" << height + << " (rotation: " << rotation << "°)" << std::endl; + } + + // Process the frame (save to file, encode, analyze, etc.) + SaveToRawYUVFile(data); + + frameCount++; +} + +void ZoomSDKRendererDelegate::onRawDataStatusChanged(RawDataStatus status) { + switch (status) { + case RawData_On: + std::cout << "[VIDEO] Raw data ON" << std::endl; + break; + case RawData_Off: + std::cout << "[VIDEO] Raw data OFF" << std::endl; + break; + default: + std::cout << "[VIDEO] Status: " << status << std::endl; + break; + } +} + +void ZoomSDKRendererDelegate::onRendererBeDestroyed() { + std::cout << "[VIDEO] Renderer being destroyed" << std::endl; +} + +void ZoomSDKRendererDelegate::SaveToRawYUVFile(YUVRawDataI420* data) { + // Open output file in append binary mode + std::ofstream outputFile("output.yuv", std::ios::out | std::ios::binary | std::ios::app); + if (!outputFile.is_open()) { + std::cerr << "[VIDEO] Error opening output.yuv" << std::endl; + return; + } + + // YUV420 format: Y plane + U plane + V plane + size_t ySize = data->GetStreamWidth() * data->GetStreamHeight(); + size_t uvSize = ySize / 4; // U and V are quarter size each + + // Write planes in order: Y, U, V + outputFile.write(data->GetYBuffer(), ySize); + outputFile.write(data->GetUBuffer(), uvSize); + outputFile.write(data->GetVBuffer(), uvSize); + + outputFile.close(); +} +``` + +--- + +### Step 5: Create Renderer and Subscribe + +```cpp +// Create renderer helper and delegate +IZoomSDKVideoSource* videoHelper = nullptr; +ZoomSDKRendererDelegate* videoDelegate = new ZoomSDKRendererDelegate(); + +SDKError err = createRenderer(&videoHelper, videoDelegate); +if (err != SDKERR_SUCCESS || !videoHelper) { + std::cerr << "[VIDEO] Failed to create renderer: " << err << std::endl; + delete videoDelegate; + return; +} + +// Set desired resolution (affects CPU/bandwidth usage) +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +// Other options: ZoomSDKResolution_90P, 180P, 360P, 720P, 1080P + +// Subscribe to user's video stream +err = videoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO); +if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] Subscribe failed: " << err << std::endl; + return; +} + +std::cout << "[VIDEO] Successfully subscribed! Frames will arrive in onRawDataFrameReceived()" << std::endl; +``` + +**Important notes**: +- Use `createRenderer()` (global function), not `CreateRenderer()` or `new` +- Set resolution BEFORE subscribing +- Higher resolutions = more CPU/bandwidth but better quality +- Frames arrive asynchronously in `onRawDataFrameReceived()` + +--- + +## YUV420 (I420) Format Explained + +### What is YUV420? + +YUV420 separates image into: +- **Y (luma)**: Brightness information (full resolution) +- **U (Cb, blue-difference)**: Color information (quarter resolution) +- **V (Cr, red-difference)**: Color information (quarter resolution) + +### Memory Layout + +For a 1920x1080 frame: + +``` +Total bytes: 1920 * 1080 * 1.5 = 3,110,400 bytes + +Y plane: [0 to 2,073,599] (1920 * 1080 = 2,073,600 bytes) +U plane: [2,073,600 to 2,592,639] (960 * 540 = 518,400 bytes) +V plane: [2,592,640 to 3,110,399] (960 * 540 = 518,400 bytes) +``` + +**Formula**: +```cpp +size_t ySize = width * height; +size_t uSize = (width / 2) * (height / 2) = width * height / 4; +size_t vSize = (width / 2) * (height / 2) = width * height / 4; +size_t totalSize = ySize + uSize + vSize = width * height * 1.5; +``` + +### Accessing Frame Data + +```cpp +void ProcessFrame(YUVRawDataI420* data) { + int width = data->GetStreamWidth(); // e.g., 1920 + int height = data->GetStreamHeight(); // e.g., 1080 + + // Get pointers to each plane + const char* yBuffer = data->GetYBuffer(); // Brightness + const char* uBuffer = data->GetUBuffer(); // Blue-difference + const char* vBuffer = data->GetVBuffer(); // Red-difference + + // Calculate sizes + size_t ySize = width * height; + size_t uvSize = (width / 2) * (height / 2); + + // Example: Access pixel at (x, y) + int x = 100, y = 50; + + // Y value (full resolution) + unsigned char yValue = yBuffer[y * width + x]; + + // U and V values (subsampled 2x2) + unsigned char uValue = uBuffer[(y/2) * (width/2) + (x/2)]; + unsigned char vValue = vBuffer[(y/2) * (width/2) + (x/2)]; + + std::cout << "Pixel (" << x << "," << y << "): " + << "Y=" << (int)yValue << " " + << "U=" << (int)uValue << " " + << "V=" << (int)vValue << std::endl; +} +``` + +### Converting YUV420 to RGB + +```cpp +void YUVtoRGB(unsigned char y, unsigned char u, unsigned char v, + unsigned char& r, unsigned char& g, unsigned char& b) { + int c = y - 16; + int d = u - 128; + int e = v - 128; + + int red = (298 * c + 409 * e + 128) >> 8; + int green = (298 * c - 100 * d - 208 * e + 128) >> 8; + int blue = (298 * c + 516 * d + 128) >> 8; + + // Clamp to [0, 255] + r = (red < 0) ? 0 : (red > 255) ? 255 : red; + g = (green < 0) ? 0 : (green > 255) ? 255 : green; + b = (blue < 0) ? 0 : (blue > 255) ? 255 : blue; +} +``` + +--- + +## Complete Example: Save to File + +### Save Raw YUV File + +```cpp +void SaveToRawYUVFile(YUVRawDataI420* data) { + std::ofstream file("output.yuv", std::ios::binary | std::ios::app); + + size_t ySize = data->GetStreamWidth() * data->GetStreamHeight(); + size_t uvSize = ySize / 4; + + file.write(data->GetYBuffer(), ySize); + file.write(data->GetUBuffer(), uvSize); + file.write(data->GetVBuffer(), uvSize); + + file.close(); +} +``` + +**Play back with ffplay**: +```bash +ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 -framerate 30 output.yuv +``` + +### Save as PNG Images (Using OpenCV) + +```cpp +#include + +void SaveAsPNG(YUVRawDataI420* data, int frameNumber) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Create YUV Mat + cv::Mat yuvImage(height + height/2, width, CV_8UC1); + + // Copy Y plane + memcpy(yuvImage.data, data->GetYBuffer(), width * height); + + // Copy U plane + memcpy(yuvImage.data + width * height, data->GetUBuffer(), width * height / 4); + + // Copy V plane + memcpy(yuvImage.data + width * height * 5/4, data->GetVBuffer(), width * height / 4); + + // Convert to BGR + cv::Mat bgrImage; + cv::cvtColor(yuvImage, bgrImage, cv::COLOR_YUV2BGR_I420); + + // Save as PNG + std::string filename = "frame_" + std::to_string(frameNumber) + ".png"; + cv::imwrite(filename, bgrImage); +} +``` + +--- + +## Handling Video Rotation + +Video streams may be rotated (0°, 90°, 180°, 270°): + +```cpp +void ProcessFrame(YUVRawDataI420* data) { + int rotation = data->GetRotation(); // 0, 90, 180, or 270 + + if (rotation == 0) { + // Normal orientation, no rotation needed + SaveFrame(data); + } else { + // Need to rotate frame before displaying/processing + RotateFrame(data, rotation); + SaveFrame(data); + } +} +``` + +**OpenCV rotation**: +```cpp +cv::Mat RotateImage(cv::Mat& image, int rotation) { + cv::Mat rotated; + switch (rotation) { + case 90: + cv::rotate(image, rotated, cv::ROTATE_90_CLOCKWISE); + break; + case 180: + cv::rotate(image, rotated, cv::ROTATE_180); + break; + case 270: + cv::rotate(image, rotated, cv::ROTATE_90_COUNTERCLOCKWISE); + break; + default: + rotated = image; + break; + } + return rotated; +} +``` + +--- + +## Performance Considerations + +### Frame Rate + +Video typically arrives at: +- **720p**: ~30 fps +- **1080p**: ~30 fps +- **Lower resolutions**: ~15-30 fps + +Process frames quickly to avoid dropping: +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) { + // Fast: Just save to buffer/queue + frameQueue.push(data); // Process in separate thread + + // Slow: Heavy processing here + ConvertToRGB(data); // May drop frames! + EncodeToH264(data); + SaveToDisk(data); +} +``` + +### Memory Usage + +**Per frame**: +- 720p (1280x720): 1.38 MB +- 1080p (1920x1080): 3.11 MB + +**At 30 fps**: +- 720p: ~41 MB/sec +- 1080p: ~93 MB/sec + +Use buffering and separate processing threads for high performance. + +### Resolution Selection + +```cpp +// Lower resolution = less CPU/bandwidth +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); // Good for analysis + +// Higher resolution = better quality +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); // Good for recording +``` + +--- + +## Unsubscribing and Cleanup + +### Stop Capturing Video + +```cpp +// Unsubscribe from video stream +if (videoHelper) { + videoHelper->unSubscribe(userId, RAW_DATA_TYPE_VIDEO); +} + +// Stop raw recording +if (recordingCtrl) { + recordingCtrl->StopRawRecording(); +} +``` + +### Cleanup + +```cpp +// Renderer delegate is automatically deleted by SDK +// Don't call delete on videoDelegate yourself! + +// Just null out the pointer +videoHelper = nullptr; +videoDelegate = nullptr; +``` + +--- + +## Audio Raw Data + +Audio is available in **PCM format** through separate callbacks: + +### Set Up Audio Raw Data + +```cpp +class ZoomAudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + // Called for each participant's audio (no support for telephone participants) + void onOneWayAudioRawDataReceived(AudioRawData* data_, uint32_t node_id) override { + // Process individual participant's audio + std::cout << "[AUDIO] Received audio from user: " << node_id << std::endl; + } + + // Called for mixed meeting audio (what all participants hear) + void onMixedAudioRawDataReceived(AudioRawData* data_) override { + // Process mixed audio from all participants + SavePCMData(data_); + } +}; + +// Subscribe to audio +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +ZoomAudioRawDataDelegate* audioDelegate = new ZoomAudioRawDataDelegate(); +audioHelper->subscribe(audioDelegate); +``` + +**Two audio callbacks**: +- `onOneWayAudioRawDataReceived`: Individual participant audio (excludes phone participants) +- `onMixedAudioRawDataReceived`: Combined meeting audio (what you'd hear in the meeting) + +--- + +## Screen Share Raw Data + +You can also capture screen share data (separate from video): + +```cpp +// Subscribe to share data instead of video +videoHelper->subscribe(userId, RAW_DATA_TYPE_SHARE); +``` + +The same `IZoomSDKRendererDelegate::onRawDataFrameReceived()` callback is used, but you'll receive share content instead of camera video. + +--- + +## Alpha Channel Mode (Background Removal) + +Meeting hosts with a raw streaming token can enable **alpha channel mode**, which provides a mask to remove participant backgrounds. This is useful for rendering meeting participants in custom virtual environments. + +### Requirements +- Must be meeting host with raw streaming token +- Used with raw data capture + +### Check and Enable Alpha Channel + +```cpp +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + +// Check if alpha channel mode can be enabled +if (videoCtrl->CanEnableAlphaChannelMode()) { + // Enable alpha channel mode + SDKError err = videoCtrl->EnableAlphaChannelMode(true); + if (err == SDKERR_SUCCESS) { + std::cout << "[ALPHA] Alpha channel mode enabled" << std::endl; + } +} + +// Check if currently enabled +bool isEnabled = videoCtrl->IsAlphaChannelModeEnabled(); +std::cout << "[ALPHA] Alpha mode active: " << (isEnabled ? "yes" : "no") << std::endl; +``` + +### Listen for Alpha Mode Changes + +Implement `IMeetingVideoCtrlEvent` callback: + +```cpp +class VideoCtrlEventListener : public IMeetingVideoCtrlEvent { +public: + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override { + std::cout << "[ALPHA] Alpha channel mode: " + << (isAlphaModeOn ? "ON" : "OFF") << std::endl; + } + + // ... other IMeetingVideoCtrlEvent methods +}; +``` + +### Access Alpha Data in Raw Frames + +When alpha channel mode is enabled, `YUVRawDataI420` has additional methods: + +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Get standard YUV data + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Get alpha mask (only available when alpha mode is enabled) + char* alphaBuffer = data->GetAlphaBuffer(); + unsigned int alphaLen = data->GetAlphaBufferLen(); + + if (alphaBuffer != nullptr && alphaLen > 0) { + // Alpha mask is available! + // Use it to remove background from video frame + ProcessWithAlphaMask(data, alphaBuffer, alphaLen); + } else { + // No alpha data (alpha mode not enabled or not available) + ProcessNormalFrame(data); + } +} +``` + +### Alpha Mask Format + +The alpha buffer contains a grayscale mask where: +- **White (255)** = Foreground (participant's body) +- **Black (0)** = Background (to be removed) +- **Gray values** = Edge blending + +### Use Cases + +1. **Custom Virtual Backgrounds**: Replace participant backgrounds with custom scenes +2. **Mixed Reality**: Render participants in 3D virtual environments +3. **Green Screen Alternative**: Remove backgrounds without physical green screen +4. **Video Compositing**: Layer participants over other content + +--- + +## Important Performance Note + +From official documentation: +> **Do not perform heavy operations from within the raw data callbacks.** This may cause delayed or lost data. + +**Recommended pattern**: +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) { + // Fast: Copy to queue and return immediately + Frame copy; + copy.width = data->GetStreamWidth(); + copy.height = data->GetStreamHeight(); + memcpy(copy.buffer, data->GetYBuffer(), data->GetBufferLen()); + frameQueue.enqueue(copy); // Let separate thread process + + // DON'T: Heavy processing here + // EncodeH264(data); // Blocks callback! + // SaveToNetwork(data); // Too slow! +} +``` + +--- + +## Troubleshooting + +### No Frames Received + +**Checklist**: +- [ ] Called `StartRawRecording()` or `StartRawLiveStream()` BEFORE subscribing +- [ ] Waited ~500ms after starting before subscribing +- [ ] User ID is valid (not 0, exists in participant list) +- [ ] User has their video turned on +- [ ] Subscribed to correct data type (`RAW_DATA_TYPE_VIDEO`) +- [ ] Renderer delegate implements `onRawDataFrameReceived()` correctly +- [ ] You have the required permissions (host/co-host or granted permission) + +**Test**: Subscribe to your own video stream first (easier to control) + +### Frames Look Corrupted + +**Checklist**: +- [ ] Using YUV420 (I420) format, not RGB +- [ ] Buffer size is `width * height * 1.5` bytes +- [ ] Writing/reading planes in correct order: Y, U, V +- [ ] U and V planes are quarter size each, not half size +- [ ] Handling rotation correctly + +**Test**: Save raw YUV and play with ffplay to verify format + +### Performance Issues / Dropped Frames + +**Solutions**: +- Lower resolution: `setRawDataResolution(ZoomSDKResolution_360P)` +- Process frames in separate thread +- Use frame queue/buffer +- Skip frames if processing is slow (process every 2nd or 3rd frame) + +--- + +## Complete Working Example + +See the full working implementation in: +``` +C:\tempsdk\zoom-windows-sdk-sample\src\ +├── ZoomSDKRendererDelegate.h +├── ZoomSDKRendererDelegate.cpp +└── main.cpp (OnInMeeting function) +``` + +Key files to reference: +- Renderer delegate implementation +- Subscription workflow in `OnInMeeting()` +- YUV file saving logic + +--- + +## Related Documentation + +- [Authentication Pattern](authentication-pattern.md) - How to join meetings first +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Required for callbacks +- [Interface Methods](../references/interface-methods.md) - Required virtual methods +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting guide + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/send-raw-data.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/send-raw-data.md new file mode 100644 index 00000000..b2e698a1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/send-raw-data.md @@ -0,0 +1,478 @@ +# Send Raw Data (Virtual Camera & Microphone) + +Send custom video and audio into a Zoom meeting as a virtual camera or microphone. This enables: +- Streaming pre-recorded video files +- AI-generated video/audio +- Custom video effects +- Screen capture injection +- Audio playback into meetings + +--- + +## Overview + +The SDK provides three "send" interfaces: +1. **IZoomSDKVideoSource** - Virtual camera (send video) +2. **IZoomSDKVirtualAudioMicEvent** - Virtual microphone (send audio) +3. **IZoomSDKShareSource** - Virtual share source (send screen share) + +All follow the same pattern: +1. Implement the interface +2. Pass it during meeting join/start +3. SDK calls your callbacks when ready to send + +--- + +## Send Video (Virtual Camera) + +### Interface: IZoomSDKVideoSource + +```cpp +#include + +class ZoomSDKVideoSource : public IZoomSDKVideoSource { +private: + IZoomSDKVideoSender* video_sender_ = nullptr; + string video_source_; + +public: + ZoomSDKVideoSource(string video_source) : video_source_(video_source) {} + + void onInitialize(IZoomSDKVideoSender* sender, + IList* support_cap_list, + VideoSourceCapability& suggest_cap) override { + // Store the sender - you'll use this to send frames + video_sender_ = sender; + std::cout << "Video source initialized" << std::endl; + } + + void onPropertyChange(IList* support_cap_list, + VideoSourceCapability suggest_cap) override { + // SDK suggests resolution/framerate based on network + std::cout << "Suggested: " << suggest_cap.width << "x" + << suggest_cap.height << " @ " << suggest_cap.frame << "fps" << std::endl; + } + + void onStartSend() override { + // SDK is ready - start sending frames in a separate thread + std::thread([this]() { PlayVideo(); }).detach(); + } + + void onStopSend() override { + // Stop sending frames + running_ = false; + } + + void onUninitialized() override { + video_sender_ = nullptr; + } + +private: + bool running_ = false; + + void PlayVideo() { + running_ = true; + + // Open video file with OpenCV + cv::VideoCapture cap(video_source_); + if (!cap.isOpened()) { + std::cerr << "Failed to open video: " << video_source_ << std::endl; + return; + } + + while (running_ && video_sender_) { + cv::Mat frame; + if (!cap.read(frame)) { + cap.set(cv::CAP_PROP_POS_FRAMES, 0); // Loop video + continue; + } + + // Convert BGR to YUV420 (I420) + cv::Mat yuv; + cv::cvtColor(frame, yuv, cv::COLOR_BGR2YUV_I420); + + // Send frame + char* buffer = (char*)yuv.data; + int width = frame.cols; + int height = frame.rows; + int frameLen = yuv.total() * yuv.elemSize(); + + SDKError err = video_sender_->sendVideoFrame(buffer, width, height, frameLen, 0); + if (err != SDKERR_SUCCESS) { + std::cerr << "sendVideoFrame failed: " << err << std::endl; + } + + // Control framerate (e.g., 30fps = 33ms per frame) + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + } +}; +``` + +### Register Virtual Camera + +```cpp +// Create your video source +ZoomSDKVideoSource* videoSource = new ZoomSDKVideoSource("my_video.mp4"); + +// Get the raw data helper +IZoomSDKVideoSourceHelper* videoSourceHelper = GetRawdataVideoSourceHelper(); + +// Set as external video source +videoSourceHelper->setExternalVideoSource(videoSource); + +// Join meeting - your video will be the "camera" +meetingService->Join(joinParam); +``` + +### Frame Format: YUV420 (I420) + +``` +YUV420 Layout (1920x1080 example): +┌────────────────────────┐ +│ Y plane │ 1920 x 1080 = 2,073,600 bytes +│ (luminance) │ +├────────────────────────┤ +│ U plane (Cb) │ 960 x 540 = 518,400 bytes +├────────────────────────┤ +│ V plane (Cr) │ 960 x 540 = 518,400 bytes +└────────────────────────┘ +Total: width * height * 1.5 = 3,110,400 bytes +``` + +--- + +## Send Audio (Virtual Microphone) + +### Interface: IZoomSDKVirtualAudioMicEvent + +```cpp +#include + +class ZoomSDKVirtualAudioMic : public IZoomSDKVirtualAudioMicEvent { +private: + IZoomSDKAudioRawDataSender* audio_sender_ = nullptr; + string audio_source_; + bool running_ = false; + +public: + ZoomSDKVirtualAudioMic(string audio_source) : audio_source_(audio_source) {} + + void onMicInitialize(IZoomSDKAudioRawDataSender* pSender) override { + audio_sender_ = pSender; + std::cout << "Virtual mic initialized" << std::endl; + } + + void onMicStartSend() override { + // SDK is ready - start sending audio + std::thread([this]() { PlayAudio(); }).detach(); + } + + void onMicStopSend() override { + running_ = false; + } + + void onMicUninitialized() override { + audio_sender_ = nullptr; + } + +private: + void PlayAudio() { + running_ = true; + + // Open WAV file + std::ifstream file(audio_source_, std::ios::binary); + if (!file.is_open()) { + std::cerr << "Failed to open audio: " << audio_source_ << std::endl; + return; + } + + // Skip WAV header (44 bytes for standard WAV) + file.seekg(44, std::ios::beg); + + // Audio parameters (must match your WAV file!) + const int sampleRate = 48000; // 48kHz + const size_t chunkSize = 640; // Send in 640-byte chunks + + std::vector buffer(chunkSize); + + while (running_ && audio_sender_ && file.good()) { + file.read(buffer.data(), chunkSize); + std::streamsize bytesRead = file.gcount(); + + if (bytesRead > 0) { + // Send audio chunk + SDKError err = audio_sender_->send( + buffer.data(), + bytesRead, + sampleRate, + ZoomSDKAudioChannel_Mono // or ZoomSDKAudioChannel_Stereo + ); + + if (err != SDKERR_SUCCESS) { + std::cerr << "send audio failed: " << err << std::endl; + } + } + + // Control timing based on sample rate and chunk size + // 640 bytes / 2 (16-bit) / 48000 Hz = ~6.67ms + std::this_thread::sleep_for(std::chrono::microseconds(6667)); + } + + file.close(); + } +}; +``` + +### Register Virtual Microphone + +```cpp +// Create your audio source +ZoomSDKVirtualAudioMic* virtualMic = new ZoomSDKVirtualAudioMic("my_audio.wav"); + +// Get audio helper +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); + +// Set as virtual mic +audioHelper->setExternalAudioSource(virtualMic); + +// Join meeting - your audio will be the "microphone" +meetingService->Join(joinParam); +``` + +### Audio Format: PCM 16-bit + +``` +Required format: +- Encoding: PCM (signed 16-bit little-endian) +- Sample rates: 8000, 16000, 32000, 44100, or 48000 Hz +- Channels: Mono (1) or Stereo (2) + +WAV File Structure: +┌──────────────────┐ +│ RIFF Header │ 44 bytes (skip this) +├──────────────────┤ +│ Audio Data │ PCM samples +│ (16-bit signed) │ +└──────────────────┘ +``` + +--- + +## Send Share Screen (Virtual Share) + +### Interface: IZoomSDKShareSource + +```cpp +#include + +class ZoomSDKShareSource : public IZoomSDKShareSource { +private: + IZoomSDKShareSender* share_sender_ = nullptr; + +public: + void onShareSendStarted(IZoomSDKShareSender* pSender) override { + share_sender_ = pSender; + std::thread([this]() { SendShareFrames(); }).detach(); + } + + void onShareSendStopped() override { + share_sender_ = nullptr; + } + +private: + void SendShareFrames() { + while (share_sender_) { + // Capture or generate your share content + cv::Mat frame = CaptureMyContent(); + + // Convert to YUV420 + cv::Mat yuv; + cv::cvtColor(frame, yuv, cv::COLOR_BGR2YUV_I420); + + char* buffer = (char*)yuv.data; + int width = frame.cols; + int height = frame.rows; + int frameLen = yuv.total() * yuv.elemSize(); + + share_sender_->sendShareFrame(buffer, width, height, frameLen); + + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + } +}; +``` + +### Start Virtual Share + +```cpp +// Get share helper +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); + +// Create share source +ZoomSDKShareSource* shareSource = new ZoomSDKShareSource(); + +// Start sharing with your source +shareCtrl->StartShareWithPreviewEnabled(shareSource); +``` + +--- + +## Complete Example: Video Bot + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOMSDK; + +// Global references +IMeetingService* meetingService = nullptr; +IAuthService* authService = nullptr; +ZoomSDKVideoSource* videoSource = nullptr; + +void onInMeeting() { + std::cout << "In meeting - video source active!" << std::endl; +} + +void JoinMeeting() { + CreateMeetingService(&meetingService); + + // Create video source BEFORE joining + videoSource = new ZoomSDKVideoSource("Big_Buck_Bunny.mp4"); + IZoomSDKVideoSourceHelper* helper = GetRawdataVideoSourceHelper(); + helper->setExternalVideoSource(videoSource); + + // Configure join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.psw = L"password"; + params.userName = L"Video Bot"; + params.isVideoOff = false; // Video ON to use virtual camera + params.isAudioOff = true; + + meetingService->SetEvent(new MeetingServiceEventListener(&onInMeeting)); + meetingService->Join(joinParam); +} + +void SDKAuth() { + CreateAuthService(&authService); + authService->SetEvent(new AuthServiceEventListener(&JoinMeeting)); + + AuthContext ctx; + ctx.jwt_token = L"your_jwt_token"; + authService->SDKAuth(ctx); +} + +int main() { + // Initialize + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + InitSDK(initParam); + + // Start auth flow + SDKAuth(); + + // Message loop + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +--- + +## Key Patterns + +### 1. Threading is Required + +Sending frames in callbacks will block the SDK. Always use a separate thread: + +```cpp +void onStartSend() override { + std::thread([this]() { SendFrames(); }).detach(); +} +``` + +### 2. Match Frame Rate to Network + +The SDK suggests resolution/framerate in `onPropertyChange`. Respect these suggestions: + +```cpp +void onPropertyChange(IList* caps, VideoSourceCapability suggest) override { + target_width_ = suggest.width; + target_height_ = suggest.height; + target_fps_ = suggest.frame; +} +``` + +### 3. Handle Stop/Restart Gracefully + +```cpp +void onStopSend() override { + running_ = false; // Signal thread to stop +} + +void onStartSend() override { + running_ = true; // Signal thread to start + std::thread([this]() { SendLoop(); }).detach(); +} +``` + +### 4. YUV420 Conversion + +Using OpenCV: +```cpp +cv::Mat yuv; +cv::cvtColor(bgrFrame, yuv, cv::COLOR_BGR2YUV_I420); +``` + +Manual conversion: +```cpp +// Y = 0.299*R + 0.587*G + 0.114*B +// U = -0.169*R - 0.331*G + 0.500*B + 128 +// V = 0.500*R - 0.419*G - 0.081*B + 128 +``` + +--- + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| No video showing | `isVideoOff = true` in join params | Set `isVideoOff = false` | +| Frames not sending | Blocking in callback | Use separate thread | +| Wrong colors | BGR instead of YUV | Convert to YUV420 | +| Choppy video | Wrong timing | Match suggested FPS | +| Audio glitches | Wrong sample rate | Use 48000 Hz | +| Audio silent | Wrong chunk size | Use 640 byte chunks | + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Receiving video (opposite direction) +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern +- [Authentication Pattern](authentication-pattern.md) - Complete auth workflow + +--- + +## Sample Projects + +Local samples demonstrating these concepts: +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendVideoRawData\` +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendAudioRawData\` +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendShareScreenRawData\` diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/service-quality.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/service-quality.md new file mode 100644 index 00000000..335ffaaf --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/service-quality.md @@ -0,0 +1,249 @@ +# Service Quality Monitoring + +## Overview + +Monitor video, audio, and screen sharing quality during meetings to detect network issues and notify users of unstable conditions. Works with both **Default UI** and **Custom UI**. + +--- + +## Connection Quality Enum + +All quality methods return a `ConnectionQuality` value: + +```cpp +enum ConnectionQuality { + Conn_Quality_Unknown, // Unknown connection status + Conn_Quality_Very_Bad, // Very poor quality + Conn_Quality_Bad, // Poor quality + Conn_Quality_Not_Good, // Not good quality + Conn_Quality_Normal, // Normal quality + Conn_Quality_Good, // Good quality + Conn_Quality_Excellent, // Excellent quality +}; +``` + +--- + +## Video Quality + +Get video connection quality for the local user: + +```cpp +// Get quality of video YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetVideoConnQuality(true); +std::cout << "Sent video quality: " << sentQuality << std::endl; + +// Get quality of video YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetVideoConnQuality(false); +std::cout << "Received video quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users must be sending/receiving video. + +--- + +## Audio Quality + +Get audio connection quality for the local user: + +```cpp +// Get quality of audio YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetAudioConnQuality(true); +std::cout << "Sent audio quality: " << sentQuality << std::endl; + +// Get quality of audio YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetAudioConnQuality(false); +std::cout << "Received audio quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users must be actively sending audio. + +--- + +## Screen Share Quality + +Get screen sharing connection quality for the local user: + +```cpp +// Get quality of screen share YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetSharingConnQuality(true); +std::cout << "Sent share quality: " << sentQuality << std::endl; + +// Get quality of screen share YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetSharingConnQuality(false); +std::cout << "Received share quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users in session, with at least one sharing their screen. + +--- + +## Real-Time Quality Callback + +Get quality updates for ALL users (not just local user) via the `onUserNetworkStatusChanged` callback: + +```cpp +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + void onUserNetworkStatusChanged( + MeetingComponentType type, // What type: audio, video, or share + ConnectionQuality level, // Quality level + unsigned int userId, // Which user + bool uplink // true=sending, false=receiving + ) override { + const char* typeStr = ""; + switch (type) { + case MeetingComponentType_AUDIO: typeStr = "Audio"; break; + case MeetingComponentType_VIDEO: typeStr = "Video"; break; + case MeetingComponentType_SHARE: typeStr = "Share"; break; + default: typeStr = "Unknown"; break; + } + + std::cout << "[QUALITY] User " << userId + << " " << typeStr + << " " << (uplink ? "send" : "receive") + << " quality: " << level << std::endl; + + // Alert user if quality is poor + if (level <= Conn_Quality_Bad) { + std::cout << "[WARNING] Poor " << typeStr << " quality detected!" << std::endl; + } + } + + // ... other IMeetingServiceEvent methods +}; +``` + +**MeetingComponentType enum**: +```cpp +enum MeetingComponentType { + MeetingComponentType_Def = 0, // Default/unknown + MeetingComponentType_AUDIO, // Audio component + MeetingComponentType_VIDEO, // Video component + MeetingComponentType_SHARE, // Screen share component +}; +``` + +--- + +## Detailed Statistics + +Get detailed statistics (latency, packet loss, etc.) via `ISettingService`: + +```cpp +// Get setting service +ISettingService* settingService = nullptr; +CreateSettingService(&settingService); + +if (settingService) { + IStatisticSettingContext* statsSettings = settingService->GetStatisticSettings(); + + if (statsSettings) { + // Video statistics + ASVSessionStatisticInfo videoInfo; + statsSettings->QueryVideoStatisticInfo(videoInfo); + std::cout << "Video latency: " << videoInfo.latency << "ms" << std::endl; + std::cout << "Video packet loss: " << videoInfo.packetLossAvg << "%" << std::endl; + + // Audio statistics + ASVSessionStatisticInfo audioInfo; + statsSettings->QueryAudioStatisticInfo(audioInfo); + std::cout << "Audio latency: " << audioInfo.latency << "ms" << std::endl; + + // Share statistics + ASVSessionStatisticInfo shareInfo; + statsSettings->QueryShareStatisticInfo(shareInfo); + std::cout << "Share latency: " << shareInfo.latency << "ms" << std::endl; + } +} +``` + +--- + +## Complete Example: Quality Monitor + +```cpp +class QualityMonitor : public IMeetingServiceEvent { +public: + QualityMonitor(IMeetingService* meetingService) + : m_meetingService(meetingService) {} + + void CheckQuality() { + // Check video quality + ConnectionQuality videoSend = m_meetingService->GetVideoConnQuality(true); + ConnectionQuality videoRecv = m_meetingService->GetVideoConnQuality(false); + + // Check audio quality + ConnectionQuality audioSend = m_meetingService->GetAudioConnQuality(true); + ConnectionQuality audioRecv = m_meetingService->GetAudioConnQuality(false); + + // Log current status + std::cout << "=== Connection Quality ===" << std::endl; + std::cout << "Video: Send=" << QualityToString(videoSend) + << ", Recv=" << QualityToString(videoRecv) << std::endl; + std::cout << "Audio: Send=" << QualityToString(audioSend) + << ", Recv=" << QualityToString(audioRecv) << std::endl; + + // Warn if any quality is poor + if (videoSend <= Conn_Quality_Bad || videoRecv <= Conn_Quality_Bad) { + std::cout << "[!] Video quality issues detected" << std::endl; + } + if (audioSend <= Conn_Quality_Bad || audioRecv <= Conn_Quality_Bad) { + std::cout << "[!] Audio quality issues detected" << std::endl; + } + } + + // Real-time callback + void onUserNetworkStatusChanged( + MeetingComponentType type, + ConnectionQuality level, + unsigned int userId, + bool uplink + ) override { + if (level <= Conn_Quality_Bad) { + std::cout << "[ALERT] Poor network quality for user " << userId << std::endl; + // Could trigger UI notification here + } + } + +private: + IMeetingService* m_meetingService; + + const char* QualityToString(ConnectionQuality q) { + switch (q) { + case Conn_Quality_Unknown: return "Unknown"; + case Conn_Quality_Very_Bad: return "Very Bad"; + case Conn_Quality_Bad: return "Bad"; + case Conn_Quality_Not_Good: return "Not Good"; + case Conn_Quality_Normal: return "Normal"; + case Conn_Quality_Good: return "Good"; + case Conn_Quality_Excellent: return "Excellent"; + default: return "Unknown"; + } + } + + // ... implement other required IMeetingServiceEvent methods +}; +``` + +--- + +## Use Cases + +1. **User Notifications**: Alert users when their connection quality drops +2. **Adaptive Quality**: Automatically lower video resolution when quality is poor +3. **Debugging**: Log quality metrics for troubleshooting +4. **Analytics**: Track meeting quality metrics for analysis +5. **Bot Health Monitoring**: Ensure bot has stable connection before processing + +--- + +## Related Documentation + +- [Common Issues](../troubleshooting/common-issues.md) - Network error codes +- [Raw Video Capture](raw-video-capture.md) - Video quality affects frame rate +- [Interface Methods](../references/interface-methods.md) - IMeetingServiceEvent methods + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/share-raw-data-capture.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/share-raw-data-capture.md new file mode 100644 index 00000000..8738fa50 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/share-raw-data-capture.md @@ -0,0 +1,541 @@ +# Screen Share Raw Data Capture + +## Overview + +The Meeting SDK allows you to receive raw screen share data (YUV420 frames) from participants sharing their screen. This uses the same `IZoomSDKRenderer` and `IZoomSDKRendererDelegate` pattern as video capture, but subscribes to `RAW_DATA_TYPE_SHARE` instead of `RAW_DATA_TYPE_VIDEO`. + +## Architecture + +``` +IMeetingService + ├── GetMeetingShareController() → IMeetingShareController + │ └── GetViewableSharingUserList() + │ + └── GetMeetingRecordingController() → IMeetingRecordingController + └── StartRawRecording() + +createRenderer() → IZoomSDKRenderer + ├── subscribe(userId, RAW_DATA_TYPE_SHARE) + └── unSubscribe() + +IZoomSDKRendererDelegate (your implementation) + └── onRawDataFrameReceived(YUVRawDataI420*) +``` + +## Required Headers + +```cpp +#include +#include +#include +#include +#include +``` + +## Step 1: Implement the Renderer Delegate + +```cpp +// ZoomSDKShareRendererDelegate.h +#pragma once +#include +#include +#include + +class ZoomSDKShareRendererDelegate : public ZOOMSDK::IZoomSDKRendererDelegate { +public: + ZoomSDKShareRendererDelegate(); + virtual ~ZoomSDKShareRendererDelegate(); + + // Called when a share frame is received + virtual void onRawDataFrameReceived(ZOOMSDK::YUVRawDataI420* data) override; + + // Called when renderer status changes + virtual void onRawDataStatusChanged( + ZOOMSDK::RawDataStatus status + ) override; + + // Called when resolution changes + virtual void onRendererBeDestroyed() override; + + // Frame statistics + unsigned int getFrameCount() const { return m_frameCount; } + void resetFrameCount() { m_frameCount = 0; } + + // Enable/disable file output + void enableFileOutput(const std::string& filename); + void disableFileOutput(); + +private: + unsigned int m_frameCount; + std::ofstream m_outputFile; + bool m_writeToFile; + int m_lastWidth; + int m_lastHeight; +}; +``` + +```cpp +// ZoomSDKShareRendererDelegate.cpp +#include "ZoomSDKShareRendererDelegate.h" +#include + +using namespace ZOOMSDK; + +ZoomSDKShareRendererDelegate::ZoomSDKShareRendererDelegate() + : m_frameCount(0) + , m_writeToFile(false) + , m_lastWidth(0) + , m_lastHeight(0) {} + +ZoomSDKShareRendererDelegate::~ZoomSDKShareRendererDelegate() { + disableFileOutput(); +} + +void ZoomSDKShareRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + m_frameCount++; + + // Get frame dimensions + unsigned int width = data->GetStreamWidth(); + unsigned int height = data->GetStreamHeight(); + + // Check for resolution change + if (width != m_lastWidth || height != m_lastHeight) { + std::cout << "[Share] Resolution changed: " + << width << "x" << height << std::endl; + m_lastWidth = width; + m_lastHeight = height; + } + + // Log periodically + if (m_frameCount % 30 == 0) { + std::cout << "[Share] Frame " << m_frameCount + << " - " << width << "x" << height + << std::endl; + } + + // YUV420 data access + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Calculate buffer sizes for YUV420 + unsigned int ySize = width * height; + unsigned int uvSize = (width / 2) * (height / 2); + + // Write to file if enabled + if (m_writeToFile && m_outputFile.is_open()) { + m_outputFile.write(yBuffer, ySize); + m_outputFile.write(uBuffer, uvSize); + m_outputFile.write(vBuffer, uvSize); + } + + // Process the frame (e.g., display, encode, analyze) + // The data is in YUV I420 format: + // - Y plane: width * height bytes (luminance) + // - U plane: (width/2) * (height/2) bytes (chrominance) + // - V plane: (width/2) * (height/2) bytes (chrominance) +} + +void ZoomSDKShareRendererDelegate::onRawDataStatusChanged(RawDataStatus status) { + switch (status) { + case RawData_On: + std::cout << "[Share] Raw data: ON" << std::endl; + break; + case RawData_Off: + std::cout << "[Share] Raw data: OFF" << std::endl; + break; + default: + std::cout << "[Share] Raw data status: " << status << std::endl; + } +} + +void ZoomSDKShareRendererDelegate::onRendererBeDestroyed() { + std::cout << "[Share] Renderer being destroyed" << std::endl; + disableFileOutput(); +} + +void ZoomSDKShareRendererDelegate::enableFileOutput(const std::string& filename) { + disableFileOutput(); + m_outputFile.open(filename, std::ios::binary); + if (m_outputFile.is_open()) { + m_writeToFile = true; + std::cout << "[Share] Writing to: " << filename << std::endl; + } +} + +void ZoomSDKShareRendererDelegate::disableFileOutput() { + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + m_writeToFile = false; +} +``` + +## Step 2: Initialize Share Capture + +```cpp +// Global variables +ZoomSDKShareRendererDelegate* g_shareDelegate = nullptr; +IZoomSDKRenderer* g_shareRenderer = nullptr; +IMeetingShareController* g_shareController = nullptr; +IMeetingRecordingController* g_recordController = nullptr; + +void initializeShareCapture(IMeetingService* meetingService) { + // Get controllers + g_shareController = meetingService->GetMeetingShareController(); + g_recordController = meetingService->GetMeetingRecordingController(); + + if (!g_shareController || !g_recordController) { + std::cerr << "Failed to get controllers" << std::endl; + return; + } + + // Create delegate + g_shareDelegate = new ZoomSDKShareRendererDelegate(); + + std::cout << "Share capture initialized" << std::endl; +} +``` + +## Step 3: Find Sharing User and Subscribe + +```cpp +unsigned int getSharingUserId() { + if (!g_shareController) return 0; + + // Get list of users currently sharing + IList* sharingUsers = g_shareController->GetViewableSharingUserList(); + + if (!sharingUsers || sharingUsers->GetCount() == 0) { + std::cout << "No one is sharing" << std::endl; + return 0; + } + + // Get first sharing user + unsigned int userId = sharingUsers->GetItem(0); + std::cout << "User " << userId << " is sharing" << std::endl; + + return userId; +} + +void startShareCapture() { + if (!g_shareDelegate) return; + + // Start raw recording first (required for raw data access) + SDKError err = g_recordController->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to start raw recording: " << err << std::endl; + return; + } + + // Create renderer + err = createRenderer(&g_shareRenderer, g_shareDelegate); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to create renderer: " << err << std::endl; + return; + } + + // Get sharing user + unsigned int userId = getSharingUserId(); + if (userId == 0) { + std::cout << "No sharing user to subscribe to" << std::endl; + return; + } + + // Subscribe to share raw data + err = g_shareRenderer->subscribe(userId, RAW_DATA_TYPE_SHARE); + if (err == SDKERR_SUCCESS) { + std::cout << "Subscribed to share from user " << userId << std::endl; + + // Optionally save to file + g_shareDelegate->enableFileOutput("share_capture.yuv"); + } else { + std::cerr << "Failed to subscribe: " << err << std::endl; + } +} + +void stopShareCapture() { + if (g_shareRenderer) { + g_shareRenderer->unSubscribe(); + std::cout << "Unsubscribed from share" << std::endl; + } + + if (g_shareDelegate) { + g_shareDelegate->disableFileOutput(); + } +} +``` + +## Step 4: Handle Share Events + +To know when sharing starts/stops, implement a share controller event listener: + +```cpp +// MeetingShareCtrlEventListener.h +#pragma once +#include + +class MeetingShareCtrlEventListener : public ZOOMSDK::IMeetingShareCtrlEvent { +public: + using ShareStartedCallback = std::function; + using ShareStoppedCallback = std::function; + + MeetingShareCtrlEventListener( + ShareStartedCallback onStarted = nullptr, + ShareStoppedCallback onStopped = nullptr + ); + + // Sharing status changed + virtual void onSharingStatus( + ZOOMSDK::SharingStatus status, + unsigned int userId + ) override; + + // Someone started sharing + virtual void onLockShareStatus(bool bLocked) override; + + // Share content changed + virtual void onShareContentNotification( + ZOOMSDK::ShareInfo* shareInfo + ) override; + + // Multi-share + virtual void onMultiShareSwitchToSingleShareNeedConfirm( + ZOOMSDK::IShareSwitchMultiToSingleConfirmHandler* handler + ) override; + + virtual void onShareSettingTypeChangedNotification( + ZOOMSDK::ShareSettingType type + ) override; + + virtual void onSharedVideoEnded() override; + +private: + ShareStartedCallback m_onStarted; + ShareStoppedCallback m_onStopped; +}; +``` + +```cpp +// MeetingShareCtrlEventListener.cpp +#include "MeetingShareCtrlEventListener.h" +#include + +using namespace ZOOMSDK; + +MeetingShareCtrlEventListener::MeetingShareCtrlEventListener( + ShareStartedCallback onStarted, + ShareStoppedCallback onStopped +) : m_onStarted(onStarted), m_onStopped(onStopped) {} + +void MeetingShareCtrlEventListener::onSharingStatus( + SharingStatus status, + unsigned int userId +) { + switch (status) { + case Sharing_Self_Send_Begin: + std::cout << "Started sharing (self)" << std::endl; + break; + case Sharing_Self_Send_End: + std::cout << "Stopped sharing (self)" << std::endl; + break; + case Sharing_Other_Share_Begin: + std::cout << "User " << userId << " started sharing" << std::endl; + if (m_onStarted) m_onStarted(userId); + break; + case Sharing_Other_Share_End: + std::cout << "User " << userId << " stopped sharing" << std::endl; + if (m_onStopped) m_onStopped(); + break; + case Sharing_View_Other_Sharing: + std::cout << "Viewing share from user " << userId << std::endl; + break; + case Sharing_Pause: + std::cout << "Sharing paused" << std::endl; + break; + case Sharing_Resume: + std::cout << "Sharing resumed" << std::endl; + break; + default: + std::cout << "Sharing status: " << status << std::endl; + } +} + +void MeetingShareCtrlEventListener::onLockShareStatus(bool bLocked) { + std::cout << "Share lock: " << (bLocked ? "LOCKED" : "UNLOCKED") << std::endl; +} + +void MeetingShareCtrlEventListener::onShareContentNotification(ShareInfo* shareInfo) { + if (shareInfo) { + std::cout << "Share content changed" << std::endl; + } +} + +void MeetingShareCtrlEventListener::onMultiShareSwitchToSingleShareNeedConfirm( + IShareSwitchMultiToSingleConfirmHandler* handler +) { + // Handle multi-share to single-share switch +} + +void MeetingShareCtrlEventListener::onShareSettingTypeChangedNotification( + ShareSettingType type +) { + std::cout << "Share setting changed" << std::endl; +} + +void MeetingShareCtrlEventListener::onSharedVideoEnded() { + std::cout << "Shared video ended" << std::endl; +} +``` + +## Complete Integration Example + +```cpp +// Global share event listener +MeetingShareCtrlEventListener* g_shareEventListener = nullptr; + +void onInMeeting(IMeetingService* meetingService) { + // Initialize share capture + initializeShareCapture(meetingService); + + // Set up share event listener + g_shareEventListener = new MeetingShareCtrlEventListener( + // On share started + [](unsigned int userId) { + std::cout << "Starting capture for user " << userId << std::endl; + startShareCapture(); + }, + // On share stopped + []() { + std::cout << "Stopping capture" << std::endl; + stopShareCapture(); + } + ); + + g_shareController->SetEvent(g_shareEventListener); + + // Check if someone is already sharing + unsigned int sharingUser = getSharingUserId(); + if (sharingUser != 0) { + startShareCapture(); + } +} + +// Cleanup +void cleanup() { + stopShareCapture(); + + if (g_shareRenderer) { + // Renderer will be destroyed when unsubscribed + g_shareRenderer = nullptr; + } + + delete g_shareDelegate; + g_shareDelegate = nullptr; + + delete g_shareEventListener; + g_shareEventListener = nullptr; +} +``` + +## Sharing Status Values + +```cpp +enum SharingStatus { + Sharing_Self_Send_Begin, // You started sharing + Sharing_Self_Send_End, // You stopped sharing + Sharing_Other_Share_Begin, // Someone else started sharing + Sharing_Other_Share_End, // Someone else stopped sharing + Sharing_View_Other_Sharing, // Viewing someone's share + Sharing_Pause, // Sharing paused + Sharing_Resume, // Sharing resumed + Sharing_ContentTypeChange, // Content type changed + Sharing_SelfStartAudioShare, // Started audio share + Sharing_SelfStopAudioShare // Stopped audio share +}; +``` + +## Raw Data Types + +```cpp +enum RawDataType { + RAW_DATA_TYPE_VIDEO = 0, // Video frames + RAW_DATA_TYPE_SHARE // Screen share frames +}; +``` + +## Permission Requirements + +To receive screen share raw data, you need: + +1. **Raw Recording Permission**: Call `StartRawRecording()` before subscribing +2. **Recording Permission**: Either be host, co-host, or have recording permission granted + +```cpp +bool checkPermission() { + SDKError err = g_recordController->CanStartRecording(false, 0); + if (err != SDKERR_SUCCESS) { + std::cout << "Requesting recording permission..." << std::endl; + g_recordController->RequestLocalRecordingPrivilege(); + return false; + } + return true; +} +``` + +## Converting YUV to RGB/Image + +The raw data is in YUV420 (I420) format. To convert to RGB or save as image: + +```cpp +// Using OpenCV +#include + +void saveFrameAsImage(YUVRawDataI420* data, const std::string& filename) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Create Mat from YUV data + cv::Mat yuvFrame(height + height/2, width, CV_8UC1); + + // Copy Y plane + memcpy(yuvFrame.data, data->GetYBuffer(), width * height); + + // Copy U and V planes (interleaved in I420) + memcpy(yuvFrame.data + width * height, + data->GetUBuffer(), (width/2) * (height/2)); + memcpy(yuvFrame.data + width * height + (width/2) * (height/2), + data->GetVBuffer(), (width/2) * (height/2)); + + // Convert to BGR + cv::Mat bgrFrame; + cv::cvtColor(yuvFrame, bgrFrame, cv::COLOR_YUV2BGR_I420); + + // Save + cv::imwrite(filename, bgrFrame); +} +``` + +## Common Pitfalls + +1. **No share data**: Make sure someone is actually sharing before subscribing +2. **Raw recording**: Must call `StartRawRecording()` before subscribing to share +3. **Permission**: Recording permission is required for raw data access +4. **Multiple sharers**: Use `GetViewableSharingUserList()` to get all sharing users +5. **Resolution changes**: Screen share resolution can change dynamically (window resize) +6. **Frame rate**: Screen share frame rate is typically lower than video (varies by content) +7. **Subscribe timing**: Subscribe after sharing starts, not before + +## Difference from Video Capture + +| Aspect | Video Capture | Share Capture | +|--------|---------------|---------------| +| Data Type | `RAW_DATA_TYPE_VIDEO` | `RAW_DATA_TYPE_SHARE` | +| Source | Webcam | Screen/Window/App | +| Resolution | Fixed camera resolution | Dynamic (window size) | +| Frame Rate | Consistent (30fps) | Variable (content-dependent) | +| User List | Participants controller | Share controller | + +Both use the same `IZoomSDKRenderer` and `IZoomSDKRendererDelegate` interfaces. diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/video-advanced.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/video-advanced.md new file mode 100644 index 00000000..6bf55d09 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/examples/video-advanced.md @@ -0,0 +1,343 @@ +# Video Advanced Features: Level 3 Helpers + +## Overview + +The `IMeetingVideoController` provides three Level 3 sub-helpers for advanced video operations: + +| Helper | Getter | Platform | Purpose | +|--------|--------|----------|---------| +| `IMeetingCameraHelper` | `GetMeetingCameraHelper(userid)` | Cross-platform | Remote PTZ camera control | +| `ISetVideoOrderHelper` | `GetSetVideoOrderHelper()` | Windows-only | Batch set video order in gallery | +| `ICameraController` | `GetMyCameraController()` | Windows-only | Control local camera device | + +--- + +## Navigation Path + +``` +IMeetingService + └─► GetMeetingVideoController() + ├─► GetMeetingCameraHelper(userid) // Remote camera PTZ control + ├─► GetSetVideoOrderHelper() // Gallery video order (Windows) + └─► GetMyCameraController() // Local camera device (Windows) +``` + +--- + +## 1. Remote Camera Control (IMeetingCameraHelper) + +Control PTZ (Pan-Tilt-Zoom) cameras of remote participants who have granted permission. + +### Get the Helper + +```cpp +// Get video controller first +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +// Get camera helper for specific user +unsigned int targetUserId = 12345; +IMeetingCameraHelper* cameraHelper = videoCtrl->GetMeetingCameraHelper(targetUserId); +if (!cameraHelper) { + // User doesn't exist or doesn't have controllable camera + return; +} +``` + +### Check Controllability + +```cpp +// Check if we can control this user's camera +if (!cameraHelper->CanControlCamera()) { + // Need to request control first + SDKError err = cameraHelper->RequestControlRemoteCamera(); + if (err != SDKERR_SUCCESS) { + // Request failed - user may not have PTZ camera + } + // Wait for callback to confirm permission granted + return; +} +``` + +### Camera Movement Operations + +```cpp +// All movement methods accept range 10-100 (default: 50) +// Higher = faster/larger movement + +// Pan left/right +cameraHelper->TurnLeft(50); // Pan left +cameraHelper->TurnRight(50); // Pan right + +// Tilt up/down +cameraHelper->TurnUp(50); // Tilt up +cameraHelper->TurnDown(50); // Tilt down + +// Zoom in/out +cameraHelper->ZoomIn(50); // Zoom in +cameraHelper->ZoomOut(50); // Zoom out +``` + +### Release Control + +```cpp +// When done controlling +cameraHelper->GiveUpControlRemoteCamera(); +``` + +### Handle Camera Control Requests (Callback) + +```cpp +class MyCameraEventHandler : public IMeetingVideoCtrlEvent { +public: + void onCameraControlRequestReceived( + unsigned int userId, + CameraControlRequestType requestType, + ICameraControlRequestHandler* pHandler) override + { + if (requestType == CameraControlRequestType_RequestControl) { + // Someone wants to control our camera + // Approve or decline + pHandler->Approve(); // or pHandler->Decline(); + } else if (requestType == CameraControlRequestType_GiveUpControl) { + // User released control of our camera + } + } + + void onCameraControlRequestResult( + unsigned int userId, + CameraControlRequestResult result) override + { + switch (result) { + case CameraControlRequestResult_Approve: + // Our request was approved - can now control camera + break; + case CameraControlRequestResult_Decline: + // Our request was declined + break; + case CameraControlRequestResult_Revoke: + // Our control was revoked + break; + } + } + + // ... other callback implementations +}; +``` + +--- + +## 2. Video Order Control (ISetVideoOrderHelper) - Windows Only + +Batch-set the video order in gallery view. Useful for arranging participant videos in a specific layout. + +### Get the Helper + +```cpp +#if defined(WIN32) +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +ISetVideoOrderHelper* orderHelper = videoCtrl->GetSetVideoOrderHelper(); +if (!orderHelper) return; +#endif +``` + +### Set Video Order (Transaction Pattern) + +```cpp +#if defined(WIN32) +// Step 1: Begin transaction (clears any previous prepared order) +SDKError err = orderHelper->SetVideoOrderTransactionBegin(); +if (err != SDKERR_SUCCESS) return; + +// Step 2: Add users to positions (0-based, max 49 positions) +// Position 0 = first slot in gallery +orderHelper->AddVideoToOrder(hostUserId, 0); // Host at position 0 +orderHelper->AddVideoToOrder(presenter1Id, 1); // Presenter at position 1 +orderHelper->AddVideoToOrder(presenter2Id, 2); // Another presenter at position 2 +// ... add more as needed + +// Step 3: Commit the transaction +err = orderHelper->SetVideoOrderTransactionCommit(); +if (err != SDKERR_SUCCESS) { + // Commit failed +} +#endif +``` + +### Important Notes + +- Maximum 49 users can be ordered +- If same position assigned to multiple users, only last one applies +- Must call `SetVideoOrderTransactionBegin()` before adding users +- Only host/co-host can set video order for all participants +- Use `EnableFollowHostVideoOrder()` to make participants follow host's order + +### Follow Host Video Order + +```cpp +// Check if feature is supported +if (videoCtrl->IsSupportFollowHostVideoOrder()) { + // Enable following host's video order + videoCtrl->EnableFollowHostVideoOrder(true); + + // Check if currently following + bool isFollowing = videoCtrl->IsFollowHostVideoOrderOn(); +} + +// Get current video order list +IList* orderList = videoCtrl->GetVideoOrderList(); +if (orderList) { + for (int i = 0; i < orderList->GetCount(); i++) { + unsigned int userId = orderList->GetItem(i); + // Process ordered user IDs + } +} +``` + +--- + +## 3. Local Camera Control (ICameraController) - Windows Only + +Control the local user's camera device settings. + +### Get the Helper + +```cpp +#if defined(WIN32) +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +ICameraController* cameraCtrl = videoCtrl->GetMyCameraController(); +if (!cameraCtrl) return; + +// ICameraController provides device-level camera control +// (Interface details depend on SDK version - check headers) +#endif +``` + +--- + +## Video Order Callbacks + +```cpp +class MyVideoEventHandler : public IMeetingVideoCtrlEvent { +public: + void onHostVideoOrderUpdated(IList* orderList) override { + // Host changed the video order + // Update UI to reflect new order + if (orderList) { + for (int i = 0; i < orderList->GetCount(); i++) { + unsigned int userId = orderList->GetItem(i); + // Reorder video tiles accordingly + } + } + } + + void onLocalVideoOrderUpdated(IList* localOrderList) override { + // Local video order changed (user's personal arrangement) + } + + void onFollowHostVideoOrderChanged(bool bFollow) override { + // Following host video order setting changed + if (bFollow) { + // Now following host's order + } else { + // Using local order + } + } + + // ... other callback implementations +}; +``` + +--- + +## Complete Example: Camera Control Flow + +```cpp +class CameraControlManager : public IMeetingVideoCtrlEvent { +private: + IMeetingVideoController* m_videoCtrl = nullptr; + IMeetingCameraHelper* m_currentCameraHelper = nullptr; + +public: + void Initialize(IMeetingService* meetingService) { + m_videoCtrl = meetingService->GetMeetingVideoController(); + if (m_videoCtrl) { + m_videoCtrl->SetEvent(this); + } + } + + void RequestCameraControl(unsigned int targetUserId) { + if (!m_videoCtrl) return; + + m_currentCameraHelper = m_videoCtrl->GetMeetingCameraHelper(targetUserId); + if (!m_currentCameraHelper) { + // User not found or no controllable camera + return; + } + + if (m_currentCameraHelper->CanControlCamera()) { + // Already have control + OnCameraControlGranted(); + } else { + // Request control + m_currentCameraHelper->RequestControlRemoteCamera(); + } + } + + void OnCameraControlGranted() { + // Now can control camera + // Example: Center the camera + m_currentCameraHelper->TurnLeft(30); + m_currentCameraHelper->TurnUp(20); + } + + void ReleaseCameraControl() { + if (m_currentCameraHelper) { + m_currentCameraHelper->GiveUpControlRemoteCamera(); + m_currentCameraHelper = nullptr; + } + } + + // IMeetingVideoCtrlEvent implementations + void onCameraControlRequestResult(unsigned int userId, + CameraControlRequestResult result) override { + if (result == CameraControlRequestResult_Approve) { + OnCameraControlGranted(); + } else { + m_currentCameraHelper = nullptr; + } + } + + void onCameraControlRequestReceived(unsigned int userId, + CameraControlRequestType requestType, + ICameraControlRequestHandler* pHandler) override { + // Auto-approve camera control requests + if (requestType == CameraControlRequestType_RequestControl) { + pHandler->Approve(); + } + } + + // ... implement other required callbacks + void onUserVideoStatusChange(unsigned int userId, VideoStatus status) override {} + void onSpotlightedUserListChangeNotification(IList* lst) override {} + void onHostRequestStartVideo(IRequestStartVideoHandler* handler) override {} + void onActiveSpeakerVideoUserChanged(unsigned int userid) override {} + void onActiveVideoUserChanged(unsigned int userid) override {} + void onHostVideoOrderUpdated(IList* orderList) override {} + void onLocalVideoOrderUpdated(IList* localOrderList) override {} + void onFollowHostVideoOrderChanged(bool bFollow) override {} + void onUserVideoQualityChanged(VideoConnectionQuality quality, unsigned int userid) override {} + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override {} +}; +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Complete navigation map +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/deployment.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/deployment.md new file mode 100644 index 00000000..c063a62d --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/deployment.md @@ -0,0 +1,260 @@ +# Deployment Guide + +## Overview + +When releasing or publishing your Meeting SDK app, you need to: +1. Include SDK files from `\bin` +2. Include required Microsoft VC++ runtime libraries +3. NOT re-sign protected SDK files +4. Run cleanup command for upgrades + +--- + +## Required SDK Files + +**Development** requires files from: +- `\lib` - Link libraries +- `\h` - Header files +- `\bin` - Runtime DLLs + +**Deployment/Release** only requires: +- `\bin` - Runtime DLLs (copy to your app's directory) + +--- + +## Protected Files (DO NOT Re-sign) + +These files **cannot be re-signed or have new digital signatures added**. Doing so causes **Error 105035** and fatal errors: + +``` +CptControl.exe +CptHost.exe +CptInstall.exe +CptService.exe +CptShare.dll +zzhost.dll +zzplugin.dll +aomhost64.exe +``` + +**Solution**: Skip these files in your code signing process during build/deployment. + +--- + +## Microsoft VC++ Runtime Libraries + +You must distribute the appropriate Microsoft Visual C++ runtime libraries with your app. + +### x64 Architecture (Most Common) + +**Remove** the `bin/aomhost` directory, then copy these DLLs to the same directory as SDK DLLs: + +``` +concrt140.dll +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +vcruntime140_1.dll +api-ms-win-core-console-l1-1-0.dll +api-ms-win-core-console-l1-2-0.dll +api-ms-win-core-datetime-l1-1-0.dll +api-ms-win-core-debug-l1-1-0.dll +api-ms-win-core-errorhandling-l1-1-0.dll +api-ms-win-core-file-l1-1-0.dll +api-ms-win-core-file-l1-2-0.dll +api-ms-win-core-file-l2-1-0.dll +api-ms-win-core-handle-l1-1-0.dll +api-ms-win-core-heap-l1-1-0.dll +api-ms-win-core-interlocked-l1-1-0.dll +api-ms-win-core-libraryloader-l1-1-0.dll +api-ms-win-core-localization-l1-2-0.dll +api-ms-win-core-memory-l1-1-0.dll +api-ms-win-core-namedpipe-l1-1-0.dll +api-ms-win-core-processenvironment-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-1.dll +api-ms-win-core-profile-l1-1-0.dll +api-ms-win-core-rtlsupport-l1-1-0.dll +api-ms-win-core-string-l1-1-0.dll +api-ms-win-core-synch-l1-1-0.dll +api-ms-win-core-synch-l1-2-0.dll +api-ms-win-core-sysinfo-l1-1-0.dll +api-ms-win-core-timezone-l1-1-0.dll +api-ms-win-core-util-l1-1-0.dll +api-ms-win-crt-conio-l1-1-0.dll +api-ms-win-crt-convert-l1-1-0.dll +api-ms-win-crt-environment-l1-1-0.dll +api-ms-win-crt-filesystem-l1-1-0.dll +api-ms-win-crt-heap-l1-1-0.dll +api-ms-win-crt-locale-l1-1-0.dll +api-ms-win-crt-math-l1-1-0.dll +api-ms-win-crt-multibyte-l1-1-0.dll +api-ms-win-crt-private-l1-1-0.dll +api-ms-win-crt-process-l1-1-0.dll +api-ms-win-crt-runtime-l1-1-0.dll +api-ms-win-crt-stdio-l1-1-0.dll +api-ms-win-crt-string-l1-1-0.dll +api-ms-win-crt-time-l1-1-0.dll +api-ms-win-crt-utility-l1-1-0.dll +ucrtbase.dll +``` + +### x86 Architecture (32-bit) + +Copy these DLLs to **both** `bin` AND `bin/aomhost` directories: + +``` +concrt140.dll +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +api-ms-win-core-console-l1-1-0.dll +api-ms-win-core-console-l1-2-0.dll +api-ms-win-core-datetime-l1-1-0.dll +api-ms-win-core-debug-l1-1-0.dll +api-ms-win-core-errorhandling-l1-1-0.dll +api-ms-win-core-file-l1-1-0.dll +api-ms-win-core-file-l1-2-0.dll +api-ms-win-core-file-l2-1-0.dll +api-ms-win-core-handle-l1-1-0.dll +api-ms-win-core-heap-l1-1-0.dll +api-ms-win-core-interlocked-l1-1-0.dll +api-ms-win-core-libraryloader-l1-1-0.dll +api-ms-win-core-localization-l1-2-0.dll +api-ms-win-core-memory-l1-1-0.dll +api-ms-win-core-namedpipe-l1-1-0.dll +api-ms-win-core-processenvironment-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-1.dll +api-ms-win-core-profile-l1-1-0.dll +api-ms-win-core-rtlsupport-l1-1-0.dll +api-ms-win-core-string-l1-1-0.dll +api-ms-win-core-synch-l1-1-0.dll +api-ms-win-core-synch-l1-2-0.dll +api-ms-win-core-sysinfo-l1-1-0.dll +api-ms-win-core-timezone-l1-1-0.dll +api-ms-win-core-util-l1-1-0.dll +api-ms-win-crt-conio-l1-1-0.dll +api-ms-win-crt-convert-l1-1-0.dll +api-ms-win-crt-environment-l1-1-0.dll +api-ms-win-crt-filesystem-l1-1-0.dll +api-ms-win-crt-heap-l1-1-0.dll +api-ms-win-crt-locale-l1-1-0.dll +api-ms-win-crt-math-l1-1-0.dll +api-ms-win-crt-multibyte-l1-1-0.dll +api-ms-win-crt-private-l1-1-0.dll +api-ms-win-crt-process-l1-1-0.dll +api-ms-win-crt-runtime-l1-1-0.dll +api-ms-win-crt-stdio-l1-1-0.dll +api-ms-win-crt-string-l1-1-0.dll +api-ms-win-crt-time-l1-1-0.dll +api-ms-win-crt-utility-l1-1-0.dll +ucrtbase.dll +API-MS-Win-core-xstate-l2-1-0.dll +``` + +### ARM64 Architecture + +**Remove** the `bin/aomhost` directory, then copy these DLLs: + +``` +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_atomic_wait.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +concrt140.dll +``` + +--- + +## Upgrade Installation + +When users upgrade from an older version of your app, run this command **with administrator privileges** before installing the new version: + +```cmd +cptinstall.exe -uninstall +``` + +This ensures users who had an older package can use the share function normally. + +--- + +## Visual Studio Project Configuration + +### Output Directory +Set to the `bin` folder: +- Configuration Properties → General → Output Directory: `...\bin` + +### Include Directories +- Configuration Properties → VC++ Directories → Include Directories: `...\h` + +### Library Directories +- Configuration Properties → VC++ Directories → Library Directories: `...\lib` + +### Linker Output +- Linker → General → Output File: Your desired `.exe` location + +### Debug Information (Release builds) +- C/C++ → Debug Information Format: `None` + +--- + +## Deployment Checklist + +- [ ] Copy SDK DLLs from `\bin` to app directory +- [ ] Copy appropriate VC++ runtime DLLs for your architecture +- [ ] For x64/ARM64: Remove `bin/aomhost` directory +- [ ] For x86: Copy runtime DLLs to both `bin` and `bin/aomhost` +- [ ] Exclude protected files from code signing +- [ ] Test on clean machine without Visual Studio installed +- [ ] Include `cptinstall.exe -uninstall` in upgrade process + +--- + +## Common Deployment Issues + +### "Missing DLL" errors +**Cause**: VC++ runtime libraries not included +**Fix**: Copy all required runtime DLLs to app directory + +### Error 105035 +**Cause**: Re-signed protected SDK files +**Fix**: Don't sign `CptControl.exe`, `CptHost.exe`, `CptInstall.exe`, `CptService.exe`, `CptShare.dll`, `zzhost.dll`, `zzplugin.dll`, `aomhost64.exe` + +### Share function not working after upgrade +**Cause**: Old CPT components still registered +**Fix**: Run `cptinstall.exe -uninstall` before installing new version + +### App works on dev machine but not on target +**Cause**: Target machine missing VC++ redistributable +**Fix**: Either bundle runtime DLLs or require VC++ 2019 Redistributable installation + +--- + +## Alternative: Install VC++ Redistributable + +Instead of bundling DLLs, you can require users to install the **Microsoft Visual C++ 2019 Redistributable**: +- [Download VC++ Redistributable](https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) + +However, bundling the DLLs gives you more control and ensures compatibility. + +--- + +## Related Documentation + +- [Build Errors Guide](../troubleshooting/build-errors.md) - Project setup +- [Common Issues](../troubleshooting/common-issues.md) - Error 105035 details +- [Authentication Pattern](../examples/authentication-pattern.md) - Complete working code + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/interface-methods.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/interface-methods.md new file mode 100644 index 00000000..f545077f --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/interface-methods.md @@ -0,0 +1,489 @@ +# Virtual Method Implementation Guide + +## Overview + +The Zoom SDK requires implementing ALL pure virtual methods from interface classes, even platform-specific ones. Missing even a single method results in abstract class errors at compile time. + +This guide shows how to find required methods and implement them correctly. + +--- + +## Critical Rules + +1. **Implement ALL pure virtual methods** - No exceptions +2. **Include WIN32-conditional methods** - They're required even though they're in `#if defined(WIN32)` blocks +3. **SDK version matters** - Different versions have different required methods +4. **Use exact signatures** - Parameter names can differ, but types and order must match exactly + +--- + +## How to Find Required Methods + +### Method 1: Grep the SDK Headers + +```bash +# Find all pure virtual methods (ending with = 0) +grep "= 0" SDK/x64/h/*.h + +# Find methods in specific interface +grep "= 0" SDK/x64/h/auth_service_interface.h +grep "= 0" SDK/x64/h/meeting_service_interface.h +``` + +### Method 2: Read Compiler Errors + +When you forget to implement a method, the compiler tells you: + +``` +error C2259: 'AuthServiceEventListener': cannot instantiate abstract class +note: see declaration of 'AuthServiceEventListener' +note: due to following members: +'void IAuthServiceEvent::onNotificationServiceStatus(SDKNotificationServiceStatus,SDKNotificationServiceError)': + is abstract at auth_service_interface.h(256) +``` + +This tells you: +- **Method name**: `onNotificationServiceStatus` +- **Parameters**: `SDKNotificationServiceStatus status, SDKNotificationServiceError error` +- **Location**: Line 256 in `auth_service_interface.h` + +### Method 3: Read the Header Files Manually + +Open the interface header and look for methods marked with `= 0`: + +```cpp +class IAuthServiceEvent { +public: + virtual ~IAuthServiceEvent() {} + virtual void onAuthenticationReturn(AuthResult ret) = 0; // <-- REQUIRED (= 0) + virtual void onLogout() = 0; // <-- REQUIRED (= 0) +}; +``` + +--- + +## Complete Method Lists + +### IAuthServiceEvent (6 methods required) + +**File**: `SDK/x64/h/auth_service_interface.h` (lines 217-258) + +```cpp +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // Method 1: Authentication result (JWT token validation) + void onAuthenticationReturn(AuthResult ret) override; + + // Method 2: Login result with fail reason (for user login, not JWT) + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* pAccountInfo, LoginFailReason reason) override; + + // Method 3: Logout notification + void onLogout() override; + + // Method 4: Zoom identity expired (need to regenerate token) + void onZoomIdentityExpired() override; + + // Method 5: Zoom auth identity expiring soon (10 minutes warning) + void onZoomAuthIdentityExpired() override; + + // Method 6: WIN32 ONLY - Notification service status +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif +}; +``` + +**Important notes**: +- Methods 1-5 are cross-platform +- Method 6 is Windows-only but **MUST** be implemented on Windows +- For JWT auth, only `onAuthenticationReturn` fires; others are for user login + +--- + +### IMeetingServiceEvent (9 methods required) + +**File**: `SDK/x64/h/meeting_service_interface.h` (lines 830-897) + +```cpp +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + // Method 1: Meeting status changed (joined, ended, failed, etc.) + void onMeetingStatusChanged(MeetingStatus status, int iResult) override; + + // Method 2: Meeting statistics warning (network issues, etc.) + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override; + + // Method 3: Meeting parameters (right before meeting starts) + void onMeetingParameterNotification(const MeetingParameter* meeting_param) override; + + // Method 4: Participants activities suspended + void onSuspendParticipantsActivities() override; + + // Method 5: AI Companion status changed + void onAICompanionActiveChangeNotice(bool bActive) override; + + // Method 6: Meeting topic changed + void onMeetingTopicChanged(const zchar_t* sTopic) override; + + // Method 7: Meeting at capacity, provides livestream URL + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override; + + // Method 8: User network quality changed + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, unsigned int userId, bool uplink) override; + + // Method 9: WIN32 ONLY - App signal panel updated +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override; +#endif +}; +``` + +**Important notes**: +- Methods 1-8 are cross-platform +- Method 9 is Windows-only but **MUST** be implemented on Windows +- Most apps only care about method 1 (`onMeetingStatusChanged`) + +--- + +## Implementation Patterns + +### Minimal Implementation (Empty Stubs) + +If you don't need a method's functionality, implement it as an empty stub: + +```cpp +void AuthServiceEventListener::onLogout() { + // We're not using user login, so this never fires + // Empty implementation is fine +} + +void MeetingServiceEventListener::onAICompanionActiveChangeNotice(bool bActive) { + // We don't care about AI Companion status + // Empty implementation is fine +} +``` + +### Logging Implementation (Recommended for Debugging) + +Add basic logging to see when callbacks fire: + +```cpp +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "[AUTH] Zoom identity expired! Need to regenerate JWT token." << std::endl; +} + +void MeetingServiceEventListener::onMeetingStatisticsWarningNotification(StatisticsWarningType type) { + std::cout << "[MEETING] Statistics warning: " << static_cast(type) << std::endl; +} +``` + +### Full Implementation (For Important Callbacks) + +```cpp +void MeetingServiceEventListener::onMeetingStatusChanged(MeetingStatus status, int iResult) { + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "[MEETING] Status: IDLE" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "[MEETING] Status: CONNECTING" << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "[MEETING] Status: IN MEETING" << std::endl; + if (onInMeetingCallback) { + onInMeetingCallback(); // Trigger custom logic + } + break; + case MEETING_STATUS_ENDED: + std::cout << "[MEETING] Status: ENDED (Reason: " << iResult << ")" << std::endl; + if (onMeetingEnded) { + onMeetingEnded(); // Trigger custom logic + } + break; + case MEETING_STATUS_FAILED: + std::cout << "[MEETING] Status: FAILED (Error: " << iResult << ")" << std::endl; + break; + default: + std::cout << "[MEETING] Status: UNKNOWN (" << status << ")" << std::endl; + break; + } +} +``` + +--- + +## Complete Header/Source File Template + +### Header File (AuthServiceEventListener.h) + +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // Constructor with callback + AuthServiceEventListener(void (*onComplete)()); + + // All 6 required methods + void onAuthenticationReturn(AuthResult ret) override; + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override; + void onLogout() override; + void onZoomIdentityExpired() override; + void onZoomAuthIdentityExpired() override; +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif + +private: + void (*onAuthComplete)(); +}; +``` + +### Source File (AuthServiceEventListener.cpp) + +```cpp +#include "AuthServiceEventListener.h" + +AuthServiceEventListener::AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + if (ret == AUTHRET_SUCCESS) { + std::cout << "[AUTH] Authentication successful!" << std::endl; + if (onAuthComplete) { + onAuthComplete(); + } + } else { + std::cout << "[AUTH] Authentication failed: " << ret << std::endl; + } +} + +void AuthServiceEventListener::onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) { + std::cout << "[AUTH] Login return (not used for JWT): " << ret << std::endl; +} + +void AuthServiceEventListener::onLogout() { + std::cout << "[AUTH] Logout" << std::endl; +} + +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "[AUTH] Zoom identity expired!" << std::endl; +} + +void AuthServiceEventListener::onZoomAuthIdentityExpired() { + std::cout << "[AUTH] Zoom auth identity expiring soon!" << std::endl; +} + +#if defined(WIN32) +void AuthServiceEventListener::onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) { + std::cout << "[AUTH] Notification service status: " << status << ", error: " << error << std::endl; +} +#endif +``` + +--- + +## Troubleshooting + +### Error: "Cannot instantiate abstract class" + +**Cause**: You're missing one or more pure virtual method implementations. + +**Solution**: +1. Read the compiler error carefully - it lists the missing methods +2. Look up the method signature in the SDK header file +3. Add the method to both your .h and .cpp files +4. Use `override` keyword to catch signature mismatches + +**Example error**: +``` +error C2259: 'AuthServiceEventListener': cannot instantiate abstract class +note: due to following members: +'void IAuthServiceEvent::onNotificationServiceStatus(...)': is abstract +``` + +**Fix**: Add the missing method: +```cpp +// In .h file +#if defined(WIN32) +void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif + +// In .cpp file +#if defined(WIN32) +void AuthServiceEventListener::onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) { + // Empty implementation is fine if you don't need this +} +#endif +``` + +### Error: "No suitable user-defined conversion" + +**Cause**: Method signature doesn't match exactly (wrong parameter types, missing const, etc.). + +**Solution**: Copy the signature EXACTLY from the SDK header file, including: +- `const` qualifiers +- Pointer vs reference (`*` vs `&`) +- Parameter order +- Return type + +### WIN32 Methods Not Compiling + +**Cause**: Forgot `#if defined(WIN32)` wrapper. + +**Solution**: Wrap WIN32-only methods in both .h and .cpp files: +```cpp +#if defined(WIN32) +void onNotificationServiceStatus(...) override; +#endif +``` + +--- + +--- + +## Custom UI Interfaces + +These interfaces are required when using Custom UI mode (`ENABLE_CUSTOMIZED_UI_FLAG`). + +### ICustomizedUIMgrEvent (3 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_ui_mgr.h` + +```cpp +class CustomUIMgrEventListener : public ICustomizedUIMgrEvent { +public: + // Method 1: Video container destroyed by SDK (e.g., meeting ended) + void onVideoContainerDestroyed(ICustomizedVideoContainer* pContainer) override; + + // Method 2: Share render destroyed by SDK + void onShareRenderDestroyed(ICustomizedShareRender* pRender) override; + + // Method 3: Immersive container destroyed by SDK + void onImmersiveContainerDestroyed() override; +}; +``` + +**Important notes**: +- These fire when the SDK destroys containers on its own (e.g., meeting ends) +- You MUST null out your pointers in these callbacks to avoid dangling references +- All 3 are always required + +--- + +### ICustomizedVideoContainerEvent (6 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_video_container.h` + +```cpp +class VideoContainerEventListener : public ICustomizedVideoContainerEvent { +public: + // Method 1: User changed for a video render element + void onRenderUserChanged(IVideoRenderElement* pElement, unsigned int userid) override; + + // Method 2: Data type changed (video, avatar, screen name) + void onRenderDataTypeChanged(IVideoRenderElement* pElement, VideoRenderDataType dataType) override; + + // Method 3: Layout notification — container resized, recompute element positions + void onLayoutNotification(RECT wnd_client_rect) override; + + // Method 4: A video render element was destroyed + void onVideoRenderElementDestroyed(IVideoRenderElement* pElement) override; + + // Method 5: Window messages from SDK child HWND (mouse, keyboard) + void onWindowMsgNotification(UINT uMsg, WPARAM wParam, LPARAM lParam) override; + + // Method 6: Video subscription failed for an element + void onSubscribeUserFail(ZoomSDKVideoSubscribeFailReason fail_reason, IVideoRenderElement* pElement) override; +}; +``` + +**Important notes**: +- `onLayoutNotification` is where you re-layout video elements after container resize +- `onWindowMsgNotification` forwards input from SDK's child HWND (see [Custom UI Architecture](../concepts/custom-ui-architecture.md)) +- `VideoRenderDataType` values: `VideoRenderData_Video`, `VideoRenderData_Avatar`, `VideoRenderData_ScreenName` +- `ZoomSDKVideoSubscribeFailReason` values: `ViewOnly`, `NotInMeeting`, `HasSubscribe1080POr720`, `HasSubscribeTwo720P`, `HasSubscribeExceededLimit`, `TooFrequentCall` + +--- + +### ICustomizedShareRenderEvent (3 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_share_render.h` + +```cpp +class ShareRenderEventListener : public ICustomizedShareRenderEvent { +public: + // Method 1: Started receiving shared content + void onSharingContentStartReceiving() override; + + // Method 2: Share source changed or sharing closed + void onSharingSourceNotification(unsigned int nShareSourceID) override; + + // Method 3: Window messages from share render's child HWND + void onWindowMsgNotification(UINT uMsg, WPARAM wParam, LPARAM lParam) override; +}; +``` + +**Important notes**: +- When `onSharingSourceNotification` fires with a new ID, call `SetShareSourceID(nShareSourceID)` and `Show()` +- When sharing stops, `nShareSourceID` will be 0 — call `Hide()` + +--- + +### Quick Reference: Custom UI Method Counts + +| Interface | Methods | File | +|-----------|---------|------| +| `ICustomizedUIMgrEvent` | 3 | `customized_ui/customized_ui_mgr.h` | +| `ICustomizedVideoContainerEvent` | 6 | `customized_ui/customized_video_container.h` | +| `ICustomizedShareRenderEvent` | 3 | `customized_ui/customized_share_render.h` | +| `ICustomizedImmersiveContainerEvent` | 1 | `customized_ui/customized_immersive_container.h` | +| **Total Custom UI methods** | **13** | | + +--- + +## SDK Version Differences + +Different SDK versions may have different required methods. This guide is for **SDK v6.7.2.26830**. + +If you're using a different version: +1. Run `grep "= 0" SDK/x64/h/auth_service_interface.h` to see your version's methods +2. Run `grep "= 0" SDK/x64/h/meeting_service_interface.h` +3. Compare against this guide to identify new or removed methods + +--- + +## Quick Reference Commands + +```bash +# List all pure virtual methods in SDK +grep "= 0" SDK/x64/h/*.h + +# Count methods per interface +grep -c "= 0" SDK/x64/h/auth_service_interface.h # Should be 6 +grep -c "= 0" SDK/x64/h/meeting_service_interface.h # Should be 9 + +# Find method signature +grep -A 5 "onAuthenticationReturn" SDK/x64/h/auth_service_interface.h + +# Verify your implementation has all methods +grep "override" src/AuthServiceEventListener.h # Should match SDK count +``` + +--- + +## Related Documentation + +- [Build Errors Guide](../troubleshooting/build-errors.md) - Header dependency issues +- [Authentication Pattern](../examples/authentication-pattern.md) - Using IAuthServiceEvent +- [Windows Reference](windows-reference.md) - General SDK setup + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/windows-reference.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/windows-reference.md new file mode 100644 index 00000000..188447f4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/references/windows-reference.md @@ -0,0 +1,790 @@ +# Meeting SDK - Windows Reference + +Complete reference for Zoom Meeting SDK on Windows including dependencies, Visual Studio setup, and troubleshooting. + +## System Requirements + +- **OS**: Windows 10 or later, Windows Server 2016+ +- **Architecture**: x86 (32-bit) and x64 (64-bit) +- **IDE**: Visual Studio 2019, 2022, or later +- **C++ Standard**: C++11 or later + +## Dependencies + +### Required Tools + +```powershell +# Install vcpkg (package manager for C++) +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install +``` + +### Required Libraries + +```powershell +# For x64 +.\vcpkg install jsoncpp:x64-windows +.\vcpkg install curl:x64-windows + +# For x86 +.\vcpkg install jsoncpp:x86-windows +.\vcpkg install curl:x86-windows + +# Set default triplet (optional) +$env:VCPKG_DEFAULT_TRIPLET = "x64-windows" +``` + +### Visual Studio Workloads + +Required workloads in Visual Studio Installer: +- Desktop development with C++ +- Windows 10/11 SDK +- C++ CMake tools (optional, for CMake projects) + +## Visual Studio Project Configuration + +### Project Properties Template + +Create a new C++ Console Application, then configure: + +#### C/C++ Settings + +**General → Additional Include Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\h +$(SolutionDir)SDK\$(PlatformTarget)\h\meeting_service_components +$(SolutionDir)SDK\$(PlatformTarget)\h\rawdata +C:\vcpkg\packages\jsoncpp_$(PlatformTarget)-windows\include +C:\vcpkg\packages\curl_$(PlatformTarget)-windows\include +``` + +**Preprocessor → Preprocessor Definitions:** +``` +WIN32 +_DEBUG (for Debug config) +_CONSOLE +_UNICODE +UNICODE +%(PreprocessorDefinitions) +``` + +**Code Generation → Runtime Library:** +- Debug: Multi-threaded Debug DLL (/MDd) +- Release: Multi-threaded DLL (/MD) + +#### Linker Settings + +**General → Additional Library Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\lib +C:\vcpkg\packages\jsoncpp_$(PlatformTarget)-windows\lib +C:\vcpkg\packages\curl_$(PlatformTarget)-windows\lib +``` + +**Input → Additional Dependencies:** +``` +sdk.lib +ws2_32.lib +%(AdditionalDependencies) +``` + +#### Build Events + +**Post-Build Event → Command Line:** +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +xcopy /Y /D "$(ProjectDir)config.json" "$(OutDir)" +``` + +This copies all SDK DLLs and config.json to your output directory. + +### CMakeLists.txt Template (Alternative) + +```cmake +cmake_minimum_required(VERSION 3.16) + +project(ZoomMeetingSDK CXX) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Set output directories +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) + +# Find vcpkg packages +find_package(jsoncpp CONFIG REQUIRED) +find_package(CURL REQUIRED) + +# Include directories +include_directories( + ${CMAKE_SOURCE_DIR}/SDK/x64/h + ${CMAKE_SOURCE_DIR}/SDK/x64/h/meeting_service_components + ${CMAKE_SOURCE_DIR}/SDK/x64/h/rawdata +) + +# Link directories +link_directories( + ${CMAKE_SOURCE_DIR}/SDK/x64/lib + ${CMAKE_SOURCE_DIR}/SDK/x64/bin +) + +# Source files +add_executable(YourApp + YourApp.cpp + AuthServiceEventListener.cpp + MeetingServiceEventListener.cpp + NetworkConnectionHandler.cpp + WebService.cpp + # Add more source files as needed +) + +# Link libraries +target_link_libraries(YourApp + sdk.lib + JsonCpp::JsonCpp + CURL::libcurl +) + +# Copy DLLs to output directory +add_custom_command(TARGET YourApp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/SDK/x64/bin" + $ +) + +# Copy config.json +configure_file( + ${CMAKE_SOURCE_DIR}/config.json + ${CMAKE_SOURCE_DIR}/bin/config.json + COPYONLY +) +``` + +## Configuration File + +### config.json Format + +```json +{ + "sdk_jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "meeting_number": "1234567890", + "passcode": "abc123", + "video_source": "Big_Buck_Bunny_720_10s_1MB.mp4", + "zak": "" +} +``` + +### Reading Config in Code + +```cpp +#include +#include + +void LoadConfig() { + std::ifstream f("config.json"); + Json::Value config; + + if (f.is_open()) { + try { + f >> config; + + // Extract values + std::string jwt = config["sdk_jwt"].asString(); + std::string meetingNum = config["meeting_number"].asString(); + std::string passcode = config["passcode"].asString(); + + // Convert to wstring for SDK + sdk_jwt = std::wstring(jwt.begin(), jwt.end()); + meeting_number = std::stoull(meetingNum); + // ... + } catch (const std::exception& e) { + std::cerr << "Error parsing config.json: " << e.what() << std::endl; + } + } else { + std::cerr << "config.json not found" << std::endl; + } +} +``` + +## Event Listeners + +### Complete Event Listener Templates + +#### AuthServiceEventListener.cpp + +```cpp +#include "AuthServiceEventListener.h" +#include + +AuthServiceEventListener::AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + switch (ret) { + case AUTHRET_SUCCESS: + std::cout << "Authentication successful" << std::endl; + if (onAuthComplete) onAuthComplete(); + break; + case AUTHRET_KEYORSECRETEMPTY: + std::cerr << "SDK Key or Secret is empty" << std::endl; + break; + case AUTHRET_JWTTOKENWRONG: + std::cerr << "JWT token is invalid" << std::endl; + break; + case AUTHRET_OVERTIME: + std::cerr << "Authentication timeout" << std::endl; + break; + default: + std::cerr << "Authentication failed: " << ret << std::endl; + } +} + +void AuthServiceEventListener::onLoginReturnWithReason( + LOGINSTATUS ret, + IAccountInfo* info, + LoginFailReason reason) { + // Handle login status +} + +void AuthServiceEventListener::onLogout() { + std::cout << "Logged out" << std::endl; +} + +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "Zoom identity expired" << std::endl; +} + +void AuthServiceEventListener::onZoomAuthIdentityExpired() { + std::cout << "Zoom auth identity expired - need to re-authenticate" << std::endl; +} +``` + +#### MeetingServiceEventListener.cpp + +```cpp +#include "MeetingServiceEventListener.h" +#include + +MeetingServiceEventListener::MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() +) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + +void MeetingServiceEventListener::onMeetingStatusChanged( + MeetingStatus status, + int iResult) { + + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "Meeting status: IDLE" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "Meeting status: CONNECTING" << std::endl; + if (onMeetingJoined) onMeetingJoined(); + break; + case MEETING_STATUS_WAITINGFORHOST: + std::cout << "Meeting status: WAITING FOR HOST" << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "Meeting status: IN MEETING" << std::endl; + if (onInMeetingCallback) onInMeetingCallback(); + break; + case MEETING_STATUS_DISCONNECTING: + std::cout << "Meeting status: DISCONNECTING" << std::endl; + break; + case MEETING_STATUS_RECONNECTING: + std::cout << "Meeting status: RECONNECTING" << std::endl; + break; + case MEETING_STATUS_FAILED: + std::cerr << "Meeting status: FAILED (code: " << iResult << ")" << std::endl; + if (onMeetingEnded) onMeetingEnded(); + break; + case MEETING_STATUS_ENDED: + std::cout << "Meeting status: ENDED" << std::endl; + if (onMeetingEnded) onMeetingEnded(); + break; + default: + std::cout << "Meeting status: UNKNOWN (" << status << ")" << std::endl; + } +} + +void MeetingServiceEventListener::onMeetingStatisticsWarningNotification( + StatisticsWarningType type) { + std::cout << "Meeting statistics warning: " << type << std::endl; +} + +void MeetingServiceEventListener::onMeetingParameterNotification( + const MeetingParameter* param) { + if (param) { + std::cout << "Meeting parameter notification" << std::endl; + } +} + +void MeetingServiceEventListener::onSuspendParticipantsActivities() { + std::cout << "Participants activities suspended" << std::endl; +} + +void MeetingServiceEventListener::onAICompanionActiveChangeNotice(bool isActive) { + std::cout << "AI Companion " << (isActive ? "activated" : "deactivated") << std::endl; +} +``` + +#### MeetingRecordingCtrlEventListener.cpp + +```cpp +#include "MeetingRecordingCtrlEventListener.h" +#include + +void MeetingRecordingCtrlEventListener::onRecordingStatus(RecordingStatus status) { + switch (status) { + case Recording_Start: + std::cout << "Recording started" << std::endl; + break; + case Recording_Stop: + std::cout << "Recording stopped" << std::endl; + break; + case Recording_DiskFull: + std::cerr << "Recording stopped - disk full" << std::endl; + break; + case Recording_Pause: + std::cout << "Recording paused" << std::endl; + break; + case Recording_Connecting: + std::cout << "Recording connecting" << std::endl; + break; + default: + std::cout << "Recording status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRecordPrivilegeChanged(bool bCanRec) { + std::cout << "Recording privilege: " << (bCanRec ? "granted" : "revoked") << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStatus(RecordingStatus status) { + std::cout << "Cloud recording status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecordingPrivilegeRequestStatus( + RequestRecPrivilegeStatus status) { + std::cout << "Recording privilege request status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequestStatus( + RequestLocalRecordingStatus status) { + std::cout << "Local recording privilege request status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeResponse( + bool bAccept) { + std::cout << "Local recording privilege " + << (bAccept ? "accepted" : "denied") << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCustomizedLocalRecordingSourceNotification( + ICustomizedLocalRecordingLayoutHelper* layout_helper) { + std::cout << "Customized local recording source notification" << std::endl; +} +``` + +## Raw Data Requirements + +### Recording Permission + +Raw data access requires one of: +1. **Host** status +2. **Co-host** status +3. **Recording permission** from host +4. **Recording token** (app_privilege_token) + +```cpp +bool CheckAndStartRawRecording() { + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordCtrl) { + std::cerr << "Failed to get recording controller" << std::endl; + return false; + } + + SDKError canStart = recordCtrl->CanStartRecording(false, 0); + + if (canStart == SDKERR_SUCCESS) { + SDKError err = recordCtrl->StartRawRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Raw recording started" << std::endl; + return true; + } else { + std::cerr << "StartRawRecording failed: " << err << std::endl; + return false; + } + } else { + std::cout << "Need host/cohost/recording permission" << std::endl; + std::cout << "Requesting recording privilege..." << std::endl; + recordCtrl->RequestLocalRecordingPrivilege(); + return false; + } +} +``` + +### Video Resolutions + +```cpp +// Available resolutions +videoHelper->setRawDataResolution(ZoomSDKResolution_90P); // 160x90 +videoHelper->setRawDataResolution(ZoomSDKResolution_180P); // 320x180 +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); // 640x360 +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); // 1280x720 +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); // 1920x1080 +``` + +### Audio Format + +- **Format**: PCM (raw, uncompressed) +- **Sample Rate**: 32000 Hz (typical) +- **Channels**: Mono (1) or Stereo (2) +- **Bit Depth**: 16-bit +- **Byte Order**: Little-endian + +## API Reference + +### InitParam Structure + +```cpp +struct tagInitParam { + const wchar_t* strWebDomain; // "https://zoom.us" + const wchar_t* strSupportUrl; // Support URL + SDK_LANGUAGE_ID emLanguageID; // LANGUAGE_English, etc. + bool enableGenerateDump; // Enable crash dump + bool enableLogByDefault; // Enable logging + unsigned int uiLogFileSize; // Log file size (MB, default: 5) + const wchar_t* strLogFileFolder; // Custom log folder path + RawDataOptions rawdataOpts; // Raw data options + ConfigurableOptions obConfigOpts; // Config options +}; +``` + +### IAuthService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IAuthServiceEvent*)` | Set auth callback | `SDKError` | +| `SDKAuth(AuthContext&)` | Authenticate with JWT | `SDKError` | +| `GetAuthResult()` | Get auth status | `AuthResult` | +| `LogOut()` | Logout | `SDKError` | +| `GetAccountInfo()` | Get account info | `IAccountInfo*` | + +### IMeetingService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IMeetingServiceEvent*)` | Set meeting callback | `SDKError` | +| `Join(JoinParam&)` | Join meeting | `SDKError` | +| `Start(StartParam&)` | Start meeting | `SDKError` | +| `Leave(LeaveMeetingCmd)` | Leave meeting | `SDKError` | +| `GetMeetingStatus()` | Get status | `MeetingStatus` | +| `GetMeetingInfo()` | Get meeting info | `IMeetingInfo*` | +| `GetMeetingVideoController()` | Video control | `IMeetingVideoController*` | +| `GetMeetingAudioController()` | Audio control | `IMeetingAudioController*` | +| `GetMeetingRecordingController()` | Recording control | `IMeetingRecordingController*` | +| `GetMeetingParticipantsController()` | Participants | `IMeetingParticipantsController*` | +| `GetMeetingChatController()` | Chat control | `IMeetingChatController*` | + +### JoinParam4WithoutLogin Structure + +```cpp +struct JoinParam4WithoutLogin { + UINT64 meetingNumber; // Meeting number + const wchar_t* userName; // Display name + const wchar_t* psw; // Meeting password + const wchar_t* vanityID; // Personal link name + const wchar_t* customer_key; // Customer key + const wchar_t* webinarToken; // Webinar token + const wchar_t* userZAK; // Zoom Access Key + const wchar_t* app_privilege_token; // App privilege token (for raw data) + const wchar_t* onBehalfToken; // On behalf token + bool isVideoOff; // Start with video off + bool isAudioOff; // Start with audio off + bool isDirectShareDesktop; // Share desktop directly + bool isAudioAutoConnect; // Auto-connect to audio + bool isAutoRecording; // Auto-start recording +}; +``` + +### IZoomSDKRenderer Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `setRawDataResolution(ZoomSDKResolution)` | Set resolution | `SDKError` | +| `subscribe(uint32_t userId, ZoomSDKRawDataType)` | Subscribe to video | `SDKError` | +| `unSubscribe()` | Unsubscribe | `SDKError` | +| `getResolution()` | Get resolution | `ZoomSDKResolution` | +| `getSubscribeId()` | Get user ID | `uint32_t` | + +### YUVRawDataI420 Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetStreamWidth()` | Video width | `uint32_t` | +| `GetStreamHeight()` | Video height | `uint32_t` | +| `GetYBuffer()` | Y plane buffer | `char*` | +| `GetUBuffer()` | U plane buffer | `char*` | +| `GetVBuffer()` | V plane buffer | `char*` | +| `GetBufferLen()` | Total buffer length | `uint32_t` | +| `GetRotation()` | Rotation angle | `int` | + +**Video Format:** +- YUV420 (I420) planar format +- Y plane: `width * height` bytes +- U plane: `(width/2) * (height/2)` bytes +- V plane: `(width/2) * (height/2)` bytes +- Total size: `width * height * 1.5` bytes + +### AudioRawData Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetBuffer()` | Audio buffer | `char*` | +| `GetBufferLen()` | Buffer length (bytes) | `uint32_t` | +| `GetSampleRate()` | Sample rate (Hz) | `uint32_t` | +| `GetChannelNum()` | Number of channels | `uint32_t` | + +### Error Codes (SDKError) + +| Code | Description | +|------|-------------| +| `SDKERR_SUCCESS` | Success | +| `SDKERR_INVALID_PARAMETER` | Invalid parameter | +| `SDKERR_UNINITIALIZE` | SDK not initialized | +| `SDKERR_UNAUTHENTICATION` | Not authenticated | +| `SDKERR_NO_PERMISSION` | No permission | +| `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device | +| `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device | +| `SDKERR_INTERNAL_ERROR` | Internal error | +| `SDKERR_SERVICE_FAILED` | Service failed | +| `SDKERR_MEMORY_FAILED` | Memory allocation failed | +| `SDKERR_TOO_FREQUENT_CALL` | API called too frequently | +| `SDKERR_WRONG_USAGE` | Wrong API usage | + +### Authentication Results (AuthResult) + +| Result | Description | +|--------|-------------| +| `AUTHRET_SUCCESS` | Authentication successful | +| `AUTHRET_KEYORSECRETEMPTY` | Key or secret empty | +| `AUTHRET_JWTTOKENWRONG` | JWT token invalid | +| `AUTHRET_OVERTIME` | Operation timed out | + +### Meeting Status (MeetingStatus) + +| Status | Description | +|--------|-------------| +| `MEETING_STATUS_IDLE` | No meeting | +| `MEETING_STATUS_CONNECTING` | Connecting | +| `MEETING_STATUS_INMEETING` | In meeting | +| `MEETING_STATUS_RECONNECTING` | Reconnecting | +| `MEETING_STATUS_FAILED` | Failed | +| `MEETING_STATUS_ENDED` | Meeting ended | +| `MEETING_STATUS_WAITINGFORHOST` | Waiting for host | +| `MEETING_STATUS_DISCONNECTING` | Disconnecting | + +## Troubleshooting + +### Common Build Errors + +#### Error: Cannot open include file 'json/json.h' + +**Cause**: jsoncpp include path not configured + +**Fix**: +1. Ensure vcpkg installed jsoncpp: `.\vcpkg install jsoncpp:x64-windows` +2. Add to **Additional Include Directories**: + ``` + C:\vcpkg\packages\jsoncpp_x64-windows\include + ``` +3. Or in Project Properties → vcpkg → Use vcpkg manifest: Yes + +#### Error: unresolved external symbol "InitSDK" + +**Cause**: sdk.lib not linked + +**Fix**: +1. Add to **Additional Library Directories**: + ``` + $(SolutionDir)SDK\$(PlatformTarget)\lib + ``` +2. Add to **Additional Dependencies**: + ``` + sdk.lib + ``` + +#### Error: sdk.dll not found when running + +**Cause**: DLLs not copied to output directory + +**Fix**: Add Post-Build Event: +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +``` + +### Runtime Errors + +#### Error: InitSDK returns SDKERR_INTERNAL_ERROR + +**Cause**: Invalid SDK initialization parameters or missing DLLs + +**Fix**: +1. Ensure all SDK DLLs are in the same directory as .exe +2. Check InitParam values are valid +3. Ensure strWebDomain is "https://zoom.us" + +#### Error: SDKAuth returns AUTHRET_JWTTOKENWRONG + +**Cause**: Invalid JWT token + +**Fix**: +1. Verify JWT token is correctly generated with SDK Key and Secret +2. Check token expiration time (iat and exp) +3. Ensure meeting number in JWT matches meeting number in join request +4. See [authorization.md](../../references/authorization.md) for JWT generation + +#### Error: Join returns SDKERR_INVALID_PARAMETER + +**Cause**: Invalid join parameters + +**Fix**: +1. Verify meeting number is correct UINT64 +2. Check meeting password is valid +3. Ensure userName is not empty +4. Convert strings to wchar_t* correctly: + ```cpp + std::wstring userName = L"Bot User"; + params.userName = userName.c_str(); + ``` + +#### Cannot start raw recording + +**Cause**: No recording permission + +**Fix**: +1. Wait for host to grant recording permission +2. Use `RequestLocalRecordingPrivilege()` to request permission +3. Or use `app_privilege_token` in JoinParam: + ```cpp + params.app_privilege_token = recording_token.c_str(); + ``` +4. Or join as host using `onBehalfToken` + +### Video/Audio Issues + +#### YUV video file is corrupted when played + +**Cause**: Mixed resolutions or incorrect parameters + +**Fix**: +1. Check console output for resolution changes +2. Create new file when resolution changes +3. Verify ffmpeg command matches actual resolution: + ```cmd + ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv output.mp4 + ``` + +#### Audio file has no sound + +**Cause**: Wrong audio format parameters + +**Fix**: +1. Check audio sample rate: `data->GetSampleRate()` +2. Verify channel count: `data->GetChannelNum()` +3. Use correct ffmpeg parameters: + ```cmd + ffplay -f s16le -ar 32000 -ac 1 audio.pcm + ``` + +#### Video subscription fails + +**Cause**: Not in meeting or no recording permission + +**Fix**: +1. Ensure you're in meeting (`MEETING_STATUS_INMEETING`) +2. Start raw recording first: `StartRawRecording()` +3. Check recording permission: `CanStartRecording()` +4. Subscribe after recording started + +## Cleanup + +```cpp +void CleanSDK() { + // Unsubscribe from raw data + if (videoHelper) { + videoHelper->unSubscribe(); + videoHelper = nullptr; + } + + if (audioHelper) { + audioHelper->unSubscribe(); + audioHelper = nullptr; + } + + // Destroy services + if (authService) { + DestroyAuthService(authService); + authService = nullptr; + } + + if (meetingService) { + DestroyMeetingService(meetingService); + meetingService = nullptr; + } + + // Clean up SDK + CleanUPSDK(); +} +``` + +## Docker Support (Windows Containers) + +### Dockerfile + +```dockerfile +# Use Windows Server Core +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +# Install Visual C++ Redistributables +ADD https://aka.ms/vs/17/release/vc_redist.x64.exe C:\vcredist.exe +RUN C:\vcredist.exe /install /quiet /norestart + +# Copy your application +WORKDIR C:\app +COPY SDK C:\app\SDK +COPY x64\Release\YourApp.exe C:\app\ +COPY config.json C:\app\ + +CMD ["C:\\app\\YourApp.exe"] +``` + +**Note**: Windows containers require Windows-based Docker host. GUI applications are supported but won't display UI. + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Options: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token`) +- **ZAK Token** - Zoom Access Key (`userZAK`) +- **On Behalf Token** - For specific use cases (`onBehalfToken`) + +See [bot-authentication.md](../../references/bot-authentication.md) for details. + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html +- **Sample code**: https://github.com/zoom/meetingsdk-windows-raw-recording-sample +- **Developer forum**: https://devforum.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/build-errors.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/build-errors.md new file mode 100644 index 00000000..dd570230 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/build-errors.md @@ -0,0 +1,409 @@ +# Common Build Errors and Solutions + +## SDK Header Dependency Issues + +The Zoom Windows SDK has several header dependency bugs that cause compilation errors. This guide covers the most common issues and their solutions. + +--- + +## Error 1: `uint32_t` Undefined + +### Symptoms + +``` +Error C2061: syntax error: identifier 'uint32_t' +Error C3646: 'GetAudioJoinType': unknown override specifier +Error C2059: syntax error: ')' +Error C2238: unexpected token(s) preceding ';' +``` + +Errors occur in SDK headers: +- `rawdata/rawdata_renderer_interface.h` (lines 57, 65) +- `meeting_service_components/meeting_participants_ctrl_interface.h` (line 139) + +### Root Cause + +The SDK headers use `uint32_t` but don't include `` where it's defined. + +### Solution + +**Add `#include ` to ALL your header files**, right after ``: + +```cpp +// YourListener.h +#pragma once +#include +#include // CRITICAL: Must come before SDK headers! +#include +``` + +**Required in**: +- All `.h` files that include SDK headers +- `main.cpp` or any `.cpp` that includes SDK headers directly + +### Critical Include Order + +```cpp +// 1. Windows header FIRST +#include + +// 2. Standard int types SECOND (for uint32_t) +#include + +// 3. Other standard headers +#include +#include + +// 4. Zoom SDK headers LAST +#include +#include +``` + +**This order is MANDATORY and must be followed in every file!** + +--- + +## Error 2: `AudioType` Undefined + +### Symptoms + +``` +Error C3646: 'GetAudioJoinType': unknown override specifier +Error C2059: syntax error: ')' +``` + +Error occurs when including: +```cpp +#include +``` + +### Root Cause + +`meeting_participants_ctrl_interface.h` uses `AudioType` enum (line 139) but doesn't include `meeting_audio_interface.h` where `AudioType` is defined. + +### Solution + +**Include `meeting_audio_interface.h` BEFORE `meeting_participants_ctrl_interface.h`**: + +```cpp +// Correct order +#include // FIRST +#include // SECOND +``` + +**Wrong order will fail**: +```cpp +// ❌ This will cause errors! +#include +#include +``` + +--- + +## Error 3: `YUVRawDataI420` Incomplete Type + +### Symptoms + +``` +Error: use of undefined type 'YUVRawDataI420' +Error: incomplete type is not allowed +``` + +### Root Cause + +`rawdata/rawdata_renderer_interface.h` only forward-declares `YUVRawDataI420`: +```cpp +class YUVRawDataI420; // Forward declaration only! +``` + +The full class definition is in `zoom_sdk_raw_data_def.h`. + +### Solution + +**Include `zoom_sdk_raw_data_def.h` in your renderer delegate header**: + +```cpp +// YourRendererDelegate.h +#pragma once +#include +#include +#include +#include // Full YUVRawDataI420 definition +``` + +--- + +## Error 4: Abstract Class Cannot Be Instantiated + +### Symptoms + +``` +Error C2259: 'MeetingServiceEventListener': cannot instantiate abstract class +Error: pure virtual function "IMeetingServiceEvent::onUserNetworkStatusChanged" has no overrider +Error: pure virtual function "IMeetingServiceEvent::onAppSignalPanelUpdated" has no overrider +``` + +### Root Cause + +Missing implementation of pure virtual methods (methods marked with `= 0`) required by SDK interfaces. + +### Solution + +**Implement ALL pure virtual methods** from the SDK interface. + +For `IMeetingServiceEvent` (SDK v6.7.2.26830): +```cpp +class MyMeetingListener : public IMeetingServiceEvent { +public: + // Required by ALL versions + void onMeetingStatusChanged(MeetingStatus status, int iResult) override; + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override; + void onMeetingParameterNotification(const MeetingParameter* param) override; + void onSuspendParticipantsActivities() override; + void onAICompanionActiveChangeNotice(bool isActive) override; + void onMeetingTopicChanged(const zchar_t* sTopic) override; + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override; + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, + unsigned int userId, bool uplink) override; + + // Required when WIN32 is defined + #if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override; + #endif +}; +``` + +### How to Find All Required Methods + +1. Open the SDK header file (e.g., `meeting_service_interface.h`) +2. Search for the interface class (e.g., `class IMeetingServiceEvent`) +3. Look for all methods marked with `= 0` (pure virtual) +4. Implement every single one + +**Example from SDK header**: +```cpp +class IMeetingServiceEvent { +public: + virtual void onMeetingStatusChanged(...) = 0; // Must implement! + virtual void onMeetingStatisticsWarningNotification(...) = 0; // Must implement! + // ... etc +}; +``` + +--- + +## Error 5: Override Specifier Did Not Override + +### Symptoms + +``` +Error C3668: method with override specifier 'override' did not override any base class methods +``` + +### Root Cause + +Method signature doesn't exactly match the base class, or the method doesn't exist in the SDK interface (usually due to conditional compilation). + +### Solution + +**Check for conditional compilation**: + +```cpp +// ❌ Wrong: Will fail if WIN32 is not defined +void onNotificationServiceStatus(...) override; + +// ✅ Correct: Match SDK's conditional compilation +#if defined(WIN32) +void onNotificationServiceStatus(...) override; +#endif +``` + +**Verify method signature matches exactly**: +- Parameter types must match exactly +- `const` qualifiers must match +- Parameter names don't matter, but types do + +--- + +## Complete Include Template + +### For Main Application File + +```cpp +// main.cpp +#include +#include +#include +#include +#include +#include +#include + +// Zoom SDK headers - ORDER MATTERS! +#include +#include +#include +#include +#include // BEFORE participants! +#include +#include +#include + +// Third-party libraries +#include + +// Your headers +#include "AuthServiceEventListener.h" +#include "MeetingServiceEventListener.h" +#include "ZoomSDKRendererDelegate.h" +``` + +### For Event Listener Headers + +```cpp +// AuthServiceEventListener.h +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // ... methods ... +}; +``` + +### For Renderer Delegate Headers + +```cpp +// ZoomSDKRendererDelegate.h +#pragma once +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + // ... methods ... +}; +``` + +--- + +## Preprocessor Definitions + +### Required Definition: WIN32 + +For correct SDK interface behavior, define `WIN32` in project settings: + +**Visual Studio `.vcxproj`**: +```xml +WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) +``` + +**CMake**: +```cmake +target_compile_definitions(YourTarget PRIVATE WIN32) +``` + +**Why**: SDK uses `#if defined(WIN32)` to conditionally include platform-specific methods. Without it, you'll miss required methods or have methods that don't exist in the interface. + +--- + +## Quick Troubleshooting Checklist + +When you get build errors: + +- [ ] Is `` the FIRST include? +- [ ] Is `` included after `` in ALL headers? +- [ ] Is `` included BEFORE any SDK headers? +- [ ] Is `meeting_audio_interface.h` included before `meeting_participants_ctrl_interface.h`? +- [ ] Is `zoom_sdk_raw_data_def.h` included for raw data delegates? +- [ ] Are ALL pure virtual methods implemented? +- [ ] Is `WIN32` defined in preprocessor definitions? +- [ ] Do method signatures exactly match the SDK interface (including `const`)? +- [ ] Are conditional methods (`#if defined(WIN32)`) handled correctly? + +--- + +## Common Patterns for Each Error + +### Pattern: "identifier 'X' is undefined" +→ Missing include for header that defines `X` +→ Wrong include order (SDK header before ``) + +### Pattern: "unknown override specifier" +→ Type used in method signature is undefined +→ Usually means missing include or wrong include order + +### Pattern: "cannot instantiate abstract class" +→ Missing pure virtual method implementation +→ Check SDK header for ALL methods with `= 0` + +### Pattern: "incomplete type" +→ Only forward declaration available +→ Need to include header with full definition + +### Pattern: "did not override any base class methods" +→ Method doesn't exist in interface (check conditional compilation) +→ Method signature doesn't match exactly + +--- + +## SDK Version Differences + +SDK versions may have different required methods. Always check your specific SDK version's headers. + +**To check required methods**: +```bash +# Search for pure virtual methods in interface +grep "= 0" SDK/x64/h/meeting_service_interface.h +``` + +**SDK v6.7.2.26830 requirements**: +- `IMeetingServiceEvent`: 9 methods (8 + 1 WIN32-specific) +- `IAuthServiceEvent`: 6 methods (5 + 1 WIN32-specific) +- `IZoomSDKRendererDelegate`: 3 methods + +--- + +--- + +## MSBuild Command Pattern + +When building from git bash on Windows, use this invocation pattern: + +```bash +# Git bash requires unix-style path for the exe and //p: (double slash) for switches +"/c/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/MSBuild/Current/Bin/MSBuild.exe" \ + "C:\tempsdk\zoom-windows-sdk-sample\ZoomSDKSample.vcxproj" \ + //p:Configuration=Release //p:Platform=x64 +``` + +**Key gotchas:** +- Use forward slashes for the MSBuild exe path (`/c/Program Files/...`) +- Use `//p:` not `/p:` — git bash interprets single `/p` as a path +- Use `//t:Rebuild` for clean rebuilds +- The `.vcxproj` path can use either forward or backslashes + +**From cmd.exe / PowerShell** (normal Windows paths): +```cmd +"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" ^ + ZoomSDKSample.vcxproj /p:Configuration=Release /p:Platform=x64 +``` + +--- + +## See Also + +- [Windows Message Loop](windows-message-loop.md) - Runtime callback issues +- [Virtual Method Implementation](../references/interface-methods.md) - Complete method listings +- [Authentication Pattern](../examples/authentication-pattern.md) - Getting started diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/common-issues.md new file mode 100644 index 00000000..bca58b2f --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/common-issues.md @@ -0,0 +1,575 @@ +# Common Issues & Troubleshooting Checklist + +## Quick Diagnostic Workflow + +### Build Issues vs Runtime Issues + +**Build issues** = Problems during compilation (Visual Studio errors) +**Runtime issues** = Problems when running the executable (authentication, joining meetings) + +Use this flowchart: + +``` +Does your code compile? +├─ NO → See "Build Issues Checklist" below +└─ YES → Does authentication succeed? + ├─ NO → See "Authentication Issues" below + └─ YES → Does meeting join succeed? + ├─ NO → See "Meeting Join Issues" below + └─ YES → Does video capture work? + ├─ NO → See "Video Capture Issues" below + └─ YES → You're all set! +``` + +--- + +## Build Issues Checklist + +### ✅ Compiler Errors with `uint32_t`, `AudioType`, or `YUVRawDataI420` + +**Symptoms**: +- `Error C2061: syntax error: identifier 'uint32_t'` +- `Error C3646: 'GetAudioJoinType': unknown override specifier` +- `Error: use of undefined type 'YUVRawDataI420'` + +**Checklist**: +- [ ] `#include ` is the FIRST include in every file +- [ ] `#include ` is right after `` in every file +- [ ] `meeting_audio_interface.h` is included BEFORE `meeting_participants_ctrl_interface.h` +- [ ] `zoom_sdk_raw_data_def.h` is included when using raw video data + +**Fix**: See [Build Errors Guide](build-errors.md) for detailed solutions. + +--- + +### ✅ "Cannot Instantiate Abstract Class" Error + +**Symptoms**: +- `Error C2259: 'AuthServiceEventListener': cannot instantiate abstract class` +- `note: due to following members: 'void IAuthServiceEvent::onNotificationServiceStatus(...)': is abstract` + +**Checklist**: +- [ ] Implemented ALL pure virtual methods from `IAuthServiceEvent` (6 methods) +- [ ] Implemented ALL pure virtual methods from `IMeetingServiceEvent` (9 methods) +- [ ] Included WIN32-conditional methods (even though they're in `#if defined(WIN32)`) +- [ ] Used `override` keyword to catch signature mismatches +- [ ] Verified method signatures match SDK headers exactly + +**Fix**: See [Interface Methods Guide](../references/interface-methods.md) for complete method lists. + +--- + +### ✅ Linker Errors + +**Symptoms**: +- `LNK2019: unresolved external symbol` +- `Error: Cannot open file 'sdk.lib'` + +**Checklist**: +- [ ] Added SDK library directory to Project Properties → Linker → General → Additional Library Directories + - `$(SolutionDir)SDK\x64\lib` +- [ ] Added `sdk.lib` to Project Properties → Linker → Input → Additional Dependencies +- [ ] Verified SDK architecture matches project architecture (both x64) +- [ ] SDK DLL (`sdk.dll`) is in the same directory as the executable or in PATH + +**Fix**: +1. Right-click project → Properties +2. Configuration: All Configurations, Platform: x64 +3. Linker → General → Additional Library Directories: Add `$(SolutionDir)SDK\x64\lib` +4. Linker → Input → Additional Dependencies: Add `sdk.lib` +5. Copy `SDK\x64\bin\sdk.dll` to your output directory + +--- + +## Runtime Issues + +### ✅ Authentication Timeout (CRITICAL!) + +**Symptoms**: +- "Still waiting..." messages keep printing +- "ERROR: Authentication timeout after 30 seconds" +- `onAuthenticationReturn()` callback NEVER fires + +**Root Cause**: 99% of the time, this is a **missing Windows message loop**, NOT a JWT token issue! + +**Checklist**: +- [ ] Added `PeekMessage()` loop during authentication wait +- [ ] Added `PeekMessage()` loop in main event loop +- [ ] Using `PM_REMOVE` flag with `PeekMessage()` +- [ ] Calling `TranslateMessage()` and `DispatchMessage()` for each message + +**Fix**: See [Windows Message Loop Guide](windows-message-loop.md) for complete solution. + +**Quick test**: Add this minimal message loop: +```cpp +while (!g_authenticated) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +If `onAuthenticationReturn()` suddenly fires, you confirmed the issue was the message loop. + +--- + +### ✅ Authentication Fails with Error Code + +**Symptoms**: +- `onAuthenticationReturn()` fires but with non-success code +- "Authentication failed: [error code]" + +**Authentication Error Codes**: + +| Code | Enum Value | Meaning | Solution | +|------|------------|---------|----------| +| 0 | `AUTHRET_SUCCESS` | ✅ Success | N/A | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | JWT token or app secret is empty | Verify JWT token string is not empty | +| 2 | `AUTHRET_JWTTOKENWRONG` | Invalid JWT token format or signature | Regenerate JWT token, verify app credentials | +| 3 | `AUTHRET_OVERTIME` | JWT token expired | Regenerate JWT with fresh timestamp | +| 4 | `AUTHRET_NETWORKISSUE` | Network connection problem | Check firewall, proxy settings, internet connection | +| 16 | `AUTHRET_CLIENT_INCOMPATIBLE` | SDK version incompatible with Zoom service | Update SDK to latest version | + +**Checklist**: +- [ ] JWT token is correctly formatted (3 parts separated by dots) +- [ ] JWT token was generated within the last hour +- [ ] App credentials (SDK Key/Secret) match JWT token generation +- [ ] System clock is accurate (JWT validation is time-sensitive) +- [ ] Not behind a firewall blocking Zoom domains (*.zoom.us, *.zoomgov.com) + +**How to regenerate JWT token**: +```javascript +// Node.js example +const jwt = require('jsonwebtoken'); +const token = jwt.sign( + { + appKey: 'YOUR_SDK_KEY', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7200, // 2 hours + tokenExp: Math.floor(Date.now() / 1000) + 7200 + }, + 'YOUR_SDK_SECRET' +); +``` + +--- + +### ✅ Meeting Join Fails + +**Symptoms**: +- `onMeetingStatusChanged()` fires with `MEETING_STATUS_FAILED` +- "Join meeting failed: [error code]" + +**Meeting Error Codes**: + +| Code | Enum Value | Meaning | Solution | +|------|------------|---------|----------| +| 0 | `MEETING_SUCCESS` | ✅ Success | N/A | +| 1 | `MEETING_FAIL_NETWORK_ERR` | Network error | Check internet connection | +| 2 | `MEETING_FAIL_RECONNECT_ERR` | Reconnection failed | Retry joining | +| 3 | `MEETING_FAIL_MMR_ERR` | Multi-media router error | Contact Zoom support | +| 4 | `MEETING_FAIL_PASSWORD_ERR` | Wrong meeting password | Verify password | +| 5 | `MEETING_FAIL_SESSION_ERR` | Invalid meeting session | Verify meeting number | +| 6 | `MEETING_FAIL_MEETING_OVER` | Meeting has ended | Join a different meeting | +| 7 | `MEETING_FAIL_MEETING_NOT_START` | Meeting hasn't started | Wait for host to start | +| 8 | `MEETING_FAIL_MEETING_NOT_EXIST` | Invalid meeting number | Verify meeting number | +| 9 | `MEETING_FAIL_MEETING_USER_FULL` | Meeting at capacity | Wait for slot or use livestream | +| 10 | `MEETING_FAIL_CLIENT_INCOMPATIBLE` | SDK version incompatible | Update SDK | + +**Checklist**: +- [ ] Meeting number is correct (10-11 digits) +- [ ] Meeting password is correct (if required) +- [ ] Authenticated successfully before joining +- [ ] Meeting is currently active (host has started it) +- [ ] Meeting hasn't reached capacity +- [ ] Using `SDK_UT_WITHOUT_LOGIN` user type for JWT auth + +**Fix for common issues**: +```cpp +// Correct join pattern +JoinParam joinParam; +joinParam.userType = SDK_UT_WITHOUT_LOGIN; // REQUIRED for JWT auth + +JoinParam4WithoutLogin withoutLoginParam; +withoutLoginParam.meetingNumber = 1234567890; // Your meeting number +withoutLoginParam.userName = L"Bot User"; +withoutLoginParam.psw = L"meeting_password"; // Empty if no password +withoutLoginParam.vanityID = nullptr; +withoutLoginParam.customer_key = nullptr; +withoutLoginParam.webinarToken = nullptr; +withoutLoginParam.isVideoOff = false; +withoutLoginParam.isAudioOff = false; + +joinParam.param.withoutloginuserJoin = withoutLoginParam; +meetingService->Join(joinParam); +``` + +--- + +### ✅ Callbacks Not Firing + +**Symptoms**: +- `SetEvent()` called but callbacks never execute +- Authentication/meeting status changes but no output + +**Checklist**: +- [ ] Windows message loop is running (see Authentication Timeout above) +- [ ] Event listener pointer is valid (not deleted prematurely) +- [ ] Event listener is set BEFORE calling SDK methods +- [ ] Using `new` to allocate listener (SDK manages lifecycle) + +**Fix**: +```cpp +// CORRECT: Set listener before SDK actions +AuthServiceEventListener* authListener = new AuthServiceEventListener(&OnAuthComplete); +authService->SetEvent(authListener); +authService->SDKAuth(authContext); // Now callbacks will work + +// WRONG: Set listener after +authService->SDKAuth(authContext); +authService->SetEvent(authListener); // Too late! +``` + +--- + +## Video Capture Issues + +### ✅ Raw Video Data Not Received + +**Symptoms**: +- `onRawDataFrameReceived()` never fires +- `onRawDataStatusChanged()` fires but no frames + +**Checklist**: +- [ ] Called `StartRawRecording()` after joining meeting +- [ ] Subscribed to video streams using `Subscribe(userId, Raw_Video_On)` +- [ ] Implemented `IZoomSDKRendererDelegate` interface correctly +- [ ] Created raw data helper: `CreateRawdataRenderer()` +- [ ] Set renderer delegate: `setRawDataResolution(...)` and `subscribe(...)` + +**Fix**: See [Raw Video Capture Guide](../examples/raw-video-capture.md) for complete workflow. + +**Quick test**: +```cpp +// After successfully joining meeting +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); +recordingCtrl->StartRawRecording(); + +IZoomSDKVideoSource* videoSource = rawDataHelper->GetRawdataVideoSourceHelper(); +videoSource->subscribe(userId, Raw_Video_On); +``` + +--- + +### ✅ Video Data Format Issues + +**Symptoms**: +- Receiving frames but video looks corrupted +- Wrong frame size or color + +**Checklist**: +- [ ] Using YUV420 (I420) format, not RGB +- [ ] Buffer size is `width * height * 1.5` bytes (not `width * height * 3`) +- [ ] Y plane: `width * height` bytes +- [ ] U plane: `(width/2) * (height/2)` bytes +- [ ] V plane: `(width/2) * (height/2)` bytes +- [ ] Rotation is handled correctly (0°, 90°, 180°, 270°) + +**YUV420 Layout**: +``` +Width: 1920, Height: 1080 +Total bytes: 1920 * 1080 * 1.5 = 3,110,400 bytes + +Y plane: [0 to 2,073,599] (1920 * 1080 bytes) +U plane: [2,073,600 to 2,592,639] (960 * 540 bytes) +V plane: [2,592,640 to 3,110,399] (960 * 540 bytes) +``` + +--- + +## Network & Firewall Issues + +### ✅ SDK Network Requirements + +**Symptoms**: +- `AUTHRET_NETWORKISSUE` error code +- `MEETING_FAIL_NETWORK_ERR` error code +- Timeouts during initialization + +**Required Firewall Rules**: +- [ ] Allow outbound HTTPS (port 443) to `*.zoom.us` +- [ ] Allow outbound HTTPS (port 443) to `*.zoomgov.com` (for government) +- [ ] Allow UDP ports 8801-8810 for media +- [ ] Not behind a proxy requiring authentication (or proxy configured) + +**How to test connectivity**: +```bash +# Test DNS resolution +ping zoom.us +ping us01web.zoom.us + +# Test HTTPS connectivity +curl https://zoom.us +curl https://us01web.zoom.us +``` + +**Proxy configuration** (if required): +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +// Add proxy settings if needed +// initParam.proxy = ...; +``` + +--- + +## General Debugging Tips + +### Enable SDK Logging + +```cpp +InitParam initParam; +initParam.enableLogByDefault = true; // Enable logs +initParam.enableGenerateDump = true; // Enable crash dumps +``` + +Logs location: `%APPDATA%\Zoom\logs\` or `C:\Users\[username]\AppData\Roaming\Zoom\logs\` + +### Add Debug Output to Callbacks + +```cpp +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + std::cout << "[DEBUG] onAuthenticationReturn called! Code: " << ret << std::endl; + // Your logic here +} +``` + +### Use Windows Debugger + +Set breakpoints in callback methods to verify they're being called: +- `onAuthenticationReturn()` +- `onMeetingStatusChanged()` + +If breakpoints never hit → Message loop issue +If breakpoints hit but code is wrong → Logic issue + +### Verify SDK Version + +Check `SDK/x64/version.txt` or `sdk.dll` properties → Details tab + +Different versions have different: +- Required callback methods +- Error codes +- API behavior + +This guide is for **SDK v6.7.2.26830**. + +--- + +## "If You See X, Do Y" Quick Reference + +| You See | Do This | +|---------|---------| +| `uint32_t` error | Add `#include ` after `` | +| `AudioType` error | Include `meeting_audio_interface.h` before `meeting_participants_ctrl_interface.h` | +| `YUVRawDataI420` error | Include `zoom_sdk_raw_data_def.h` | +| Abstract class error | Implement ALL virtual methods (see [Interface Methods Guide](../references/interface-methods.md)) | +| Authentication timeout | Add Windows message loop (see [Message Loop Guide](windows-message-loop.md)) | +| `AUTHRET_JWTTOKENWRONG` | Regenerate JWT token with correct app credentials | +| `AUTHRET_OVERTIME` | JWT token expired, generate a fresh one | +| `MEETING_FAIL_PASSWORD_ERR` | Wrong meeting password or no password provided | +| `MEETING_FAIL_MEETING_NOT_EXIST` | Invalid meeting number, verify 10-11 digits | +| Callbacks not firing | Add Windows message loop, verify `SetEvent()` called first | +| No video frames | Call `StartRawRecording()` and `Subscribe()` after joining | + +--- + +## Complete SDK Error Code Reference + +This section provides comprehensive error code tables from official Zoom documentation. + +### Global SDK Error Codes (SDKERR_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `SDKERR_SUCCESS` | Success | +| 1 | `SDKERR_NO_IMPL` | This feature is currently not available | +| 2 | `SDKERR_WRONG_USEAGE` | Incorrect usage of the feature | +| 3 | `SDKERR_INVALID_PARAMETER` | Wrong parameter | +| 4 | `SDKERR_MODULE_LOAD_FAILED` | Loading module failed | +| 5 | `SDKERR_MEMORY_FAILED` | No memory allocated | +| 6 | `SDKERR_SERVICE_FAILED` | Internal service error | +| 7 | `SDKERR_UNINITIALIZE` | SDK is not initialized before use | +| 8 | `SDKERR_UNAUTHENTICATION` | SDK is not authorized before use | +| 9 | `SDKERR_NORECORDINGINPROCESS` | No recording is in progress | +| 10 | `SDKERR_TRANSCODER_NOFOUND` | Transcoder module is not found | +| 11 | `SDKERR_VIDEO_NOTREADY` | The video service is not ready | +| 12 | `SDKERR_NO_PERMISSION` | No permission | +| 13 | `SDKERR_UNKNOWN` | Unknown error | +| 14 | `SDKERR_OTHER_SDK_INSTANCE_RUNNING` | Another SDK instance is in progress | +| 15 | `SDKERR_INTERNAL_ERROR` | SDK internal error | +| 16 | `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device is found | +| 17 | `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device is found | +| 18 | `SDKERR_TOO_FREQUENT_CALL` | API calls too frequent | +| 19 | `SDKERR_FAIL_ASSIGN_USER_PRIVILEGE` | User cannot be assigned with the new privilege | +| 20 | `SDKERR_MEETING_DONT_SUPPORT_FEATURE` | The current meeting does not support the request feature | +| 21 | `SDKERR_MEETING_NOT_SHARE_SENDER` | The current user is not the presenter | +| 22 | `SDKERR_MEETING_YOU_HAVE_NO_SHARE` | There is no sharing | +| 23 | `SDKERR_MEETING_VIEWTYPE_PARAMETER_IS_WRONG` | Incorrect `ViewType` parameters | +| 24 | `SDKERR_MEETING_ANNOTATION_IS_OFF` | Annotation is disabled | +| 25 | `SDKERR_SETTING_OS_DONT_SUPPORT` | Current OS doesn't support the setting | +| 26 | `SDKERR_EMAIL_LOGIN_IS_DISABLED` | Email login is disabled | +| 27 | `SDKERR_HARDWARE_NOT_MEET_FOR_VB` | Computer doesn't meet minimum requirements for virtual background | +| 28 | `SDKERR_NEED_USER_CONFIRM_RECORD_DISCLAIMER` | Need to process recording disclaimer | +| 29 | `SDKERR_NO_SHARE_DATA` | There is no raw data from sharing | +| 30 | `SDKERR_SHARE_CANNOT_SUBSCRIBE_MYSELF` | Cannot subscribe to my own stream | +| 31 | `SDKERR_NOT_IN_MEETING` | Not in the meeting | + +### Authentication Error Codes (AUTHRET_*) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| 0 | `AUTHRET_SUCCESS` | Authentication success | N/A | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | SDK key or secret is empty | Verify JWT token string is not empty | +| 2 | `AUTHRET_KEYORSECRETWRONG` | SDK key or secret is incorrect | Check app credentials match | +| 3 | `AUTHRET_ACCOUNTNOTSUPPORT` | Account does not support SDK | Verify account has SDK access | +| 4 | `AUTHRET_ACCOUNTNOTENABLESDK` | Account does not have SDK enabled | Enable SDK for account | +| 5 | `AUTHRET_UNKNOWN` | Unknown error | Check logs | +| 6 | `AUTHRET_SERVICE_BUSY` | Service is busy | Retry later | +| 7 | `AUTHRET_NONE` | Initial status | N/A | +| 8 | `AUTHRET_OVERTIME` | Timeout | Check network, retry | +| 9 | `AUTHRET_NETWORKISSUE` | Network issues | Check firewall/proxy | +| 10 | `AUTHRET_CLIENT_INCOMPATIBLE` | Account does not support this SDK version | Update SDK | +| 11 | `AUTHRET_JWTTOKENWRONG` | JWT token is wrong | Regenerate JWT with correct credentials | + +### Login Error Codes (LoginFail_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `LoginFail_None` | Initial status | +| 1 | `LoginFail_EmailLoginDisable` | Email login is disabled | +| 2 | `LoginFail_UserNotExist` | User does not exist | +| 3 | `LoginFail_WrongPassword` | Incorrect password | +| 4 | `LoginFail_AccountLocked` | Account is locked | +| 5 | `LoginFail_SDKNeedUpdate` | SDK version is unsupported | +| 6 | `LoginFail_TooManyFailedAttempts` | Too many failed attempts | +| 7 | `LoginFail_SMSCodeError` | SMS verification code error | +| 8 | `LoginFail_SMSCodeExpired` | SMS verification code expired | +| 9 | `LoginFail_PhoneNumberFormatInValid` | Phone number format invalid | +| 10 | `LoginFail_LoginTokenInvalid` | Login token is invalid | +| 11 | `LoginFail_UserDisagreeLoginDisclaimer` | User disagreed login disclaimer | +| 12 | `LoginFail_Mfa_Required` | MFA is required | +| 13 | `LoginFail_Need_Bitrthday_ask` | Need to provide DOB information | +| 100 | `LoginFail_OtherIssue` | Other issue | + +### Breakout Room Error Codes (BOControllerError_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `BOControllerError_NULL_POINTER` | BO controller is null, SDK not initialized | +| 1 | `BOControllerError_WRONG_CURRENT_STATUS` | Incorrect current status | +| 2 | `BOControllerError_TOKEN_NOT_READY` | Token is not ready | +| 3 | `BOControllerError_NO_PRIVILEGE` | No privilege | +| 4 | `BOControllerError_BO_LIST_IS_UPLOADING` | BO list is uploading | +| 5 | `BOControllerError_UPLOAD_FAIL` | BO list upload failed | +| 6 | `BOControllerError_NO_ONE_HAS_BEEN_ASSIGNED` | No user assigned to BO | +| 100 | `BOControllerError_UNKNOWN` | Unknown error | + +### Phone Error Codes (PhoneFailedReason_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `PhoneFailedReason_None` | Initial status | +| 1 | `PhoneFailedReason_Busy` | Telephone service is busy | +| 2 | `PhoneFailedReason_Not_Available` | Service not available | +| 3 | `PhoneFailedReason_User_Hangup` | User hung up | +| 4 | `PhoneFailedReason_Other_Fail` | Other failure | +| 5 | `PhoneFailedReason_No_Answer` | No answer | +| 6 | `PhoneFailedReason_Block_No_Host` | Call-out blocked before host joins | +| 7 | `PhoneFailedReason_Block_High_Rate` | Blocked due to high cost | +| 8 | `PhoneFailedReason_Block_Too_Frequent` | Blocked due to high frequency | + +### OBF/Anonymous Join Error Codes (2026 Enforcement) + +**Important Dates**: +- **February 7, 2026**: OBF tokens must be valid (well-formed, not expired) +- **March 2, 2026**: Anonymous joins no longer allowed - must provide valid OBF/ZAK token + +| Code | Name | Description | +|------|------|-------------| +| 503 | `MEETING_FAIL_USER_LEVEL_TOKEN_NOT_HAVE_HOST_ZAK_OBF` | To access raw data with privilege token, must also provide OBF token authorized by host | +| 504 | `MEETING_FAIL_APP_CAN_NOT_ANONYMOUS_JOIN_MEETING` | Anonymous joins not allowed. Provide ZAK or OBF token | +| 6603 | `RESULT_UNKNOWN_ERROR` | Account is blocking your Meeting SDK application | + +### General Network/Server Error Codes + +| Code | Description | Solution | +|------|-------------|----------| +| 5 | Failed to create data connection | Check network connection | +| 15 | Failed to send create meeting command | Check HTTP request configuration | +| 1002 | Wrong user password | Check password | +| 1019 | Web login locked after 6 failed attempts | Contact support to reactivate | +| 3023 | SDK authentication failure: invalid SDK key/secret | Check credentials | +| 3024 | Account does not support using SDK | Verify license type | +| 5003 | No response from server in 30 seconds | Retry later | +| 4502 | Invalid recurring meeting - no meeting occurrence | Verify meeting recurrence | +| 5004 | DNS resolve failure | Check network adaptor | +| 102006 | Conference does not exist | Verify meeting number | +| 102011 | Client version lower than minimum required | Download latest SDK | +| 102012 | Client version higher than maximum allowed | Download latest SDK | +| 102014 | Conference token expired | Get new token | +| 103008 | Server is too busy | Retry later | +| 103024 | Account does not support requested feature | Verify account features | +| 103025 | Account does not support call out | Verify call out feature | +| 103037 | Too many pending requests | Reduce request frequency | +| 103039 | Account is in blacklist | Contact support | +| 102004/103001 | Conference already exists | Use different meeting number | +| 102010/103006 | Attendee limit reached | Contact sales for more attendees | +| 102015/103011 | Conference is locked | Contact host to unlock | +| 102016/103014 | Account restricted | Contact support | + +--- + +## File Signing Error (105035) + +**Symptom**: Error Code `105035` when running the SDK + +**Root Cause**: Re-signing or adding new signatures to protected SDK files + +**Protected Files (DO NOT re-sign)**: +- `CptControl.exe` +- `CptHost.exe` +- `CptInstall.exe` +- `CptService.exe` +- `CptShare.dll` +- `zzhost.dll` +- `zzplugin.dll` +- `aomhost64.exe` + +**Solution**: Skip signing these files during your build/deployment process. If error persists without re-signing, visit the [Zoom Developer Forum](https://devforum.zoom.us/). + +--- + +## Still Having Issues? + +1. **Check SDK logs**: `%APPDATA%\Zoom\logs\` +2. **Enable debug output**: Add `std::cout` to all callbacks +3. **Verify SDK version**: Different versions have different requirements +4. **Review working example**: See complete working code in [Authentication Pattern](../examples/authentication-pattern.md) +5. **Check Zoom Developer Forums**: https://devforum.zoom.us/ + +--- + +## Related Documentation + +- [Windows Message Loop](windows-message-loop.md) - Why callbacks don't fire +- [Build Errors Guide](build-errors.md) - Header dependency issues +- [Interface Methods Guide](../references/interface-methods.md) - Required virtual methods +- [Authentication Pattern](../examples/authentication-pattern.md) - Complete working auth code + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md new file mode 100644 index 00000000..dcfc67c5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md @@ -0,0 +1,401 @@ +# Windows Message Loop Requirement + +## Critical Issue: SDK Callbacks Not Firing + +### The Problem + +**Symptom**: Authentication times out, callbacks never execute +``` +[AUTH] Calling SDKAuth... +[AUTH] Waiting for callback... +[Still waiting after 30 seconds...] +ERROR: Authentication timeout +``` + +**Root Cause**: The Zoom Windows SDK uses the **Windows message pump** to dispatch callbacks. Without processing Windows messages, callbacks are queued but never delivered. + +--- + +## Why This Happens + +The SDK uses COM/Windows messaging for asynchronous operations: + +1. **SDK Thread**: Receives response from Zoom servers +2. **Posts Windows Message**: To application's message queue +3. **Application Must Process**: Via `GetMessage()` or `PeekMessage()` +4. **Message Dispatched**: Callback function finally invoked + +**Without message processing**: Messages queue up → Never dispatched → Callbacks never fire → Timeout + +--- + +## The Solution + +### Pattern 1: Non-Blocking with PeekMessage() (Recommended) + +Use when you need to check conditions or implement timeouts: + +```cpp +bool WaitForAuthentication() { + auto startTime = std::chrono::steady_clock::now(); + + while (!g_authenticated && !g_exit) { + // CRITICAL: Process Windows messages for SDK callbacks! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsed >= 30) { + return false; // Timeout + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} +``` + +### Pattern 2: Blocking with GetMessage() + +Use for main application loop: + +```cpp +int main() { + // ... initialize SDK, authenticate, join meeting ... + + // Main message loop + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + if (msg.message == WM_QUIT) { + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +### Pattern 3: Hybrid Approach (Our Solution) + +Combines non-blocking message processing with custom exit conditions: + +```cpp +// During authentication wait +while (!g_authenticated && !g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +// Main application loop +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Do other work here + ProcessVideoFrames(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +--- + +## Common Mistakes + +### ❌ Wrong: Just Sleeping + +```cpp +// This will NEVER work - callbacks never dispatched! +while (!g_authenticated) { + std::this_thread::sleep_for(std::chrono::seconds(1)); +} +``` + +### ❌ Wrong: Using std::condition_variable Without Messages + +```cpp +// Callbacks won't fire - no message processing! +std::unique_lock lock(mutex); +cv.wait(lock, []{ return g_authenticated; }); +``` + +### ✅ Correct: Message Loop with Condition Check + +```cpp +while (!g_authenticated) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +--- + +## Where Message Processing is Required + +You MUST process Windows messages in these scenarios: + +### 1. Authentication +```cpp +authService->SDKAuth(authContext); + +// MUST process messages while waiting +while (!authenticated) { + ProcessMessages(); +} +``` + +### 2. Joining Meeting +```cpp +meetingService->Join(joinParam); + +// MUST process messages while waiting +while (meetingStatus != IN_MEETING) { + ProcessMessages(); +} +``` + +### 3. Main Application Loop +```cpp +// MUST continuously process messages +while (!exit) { + ProcessMessages(); +} +``` + +### 4. Waiting for Any SDK Callback +Any time you're waiting for: +- `onAuthenticationReturn()` +- `onMeetingStatusChanged()` +- `onRawDataFrameReceived()` +- Any other SDK callback + +You MUST be processing Windows messages! + +--- + +## Debugging Tips + +### How to Tell if Message Loop is Missing + +**Symptoms**: +- Callbacks never fire +- Timeouts after 10-30 seconds +- No error messages from SDK +- Debug output shows "waiting..." but nothing happens + +**Quick Test**: +Add logging in your callback: +```cpp +void onAuthenticationReturn(AuthResult ret) { + std::cout << "CALLBACK FIRED!" << std::endl; // Does this ever print? +} +``` + +If you never see "CALLBACK FIRED!", you're not processing messages. + +### Debug Helper Function + +```cpp +void ProcessMessagesWithDebug() { + MSG msg; + int messageCount = 0; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + messageCount++; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + if (messageCount > 0) { + std::cout << "Processed " << messageCount << " messages" << std::endl; + } +} +``` + +--- + +## PeekMessage vs GetMessage + +| Feature | PeekMessage | GetMessage | +|---------|-------------|------------| +| **Blocking** | No | Yes | +| **Returns if no messages** | Immediately | Waits | +| **Good for** | Timeouts, conditions | Main message loop | +| **CPU Usage** | Can spin (add sleep) | Efficient | +| **Flexibility** | High | Low | + +### When to Use Each + +**PeekMessage**: +- When waiting for SDK callbacks with timeout +- When you need to check other conditions +- When combining with other work + +**GetMessage**: +- Main application message loop +- When you want efficient CPU usage +- Standard Windows application pattern + +--- + +## Complete Example + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +bool g_authenticated = false; +bool g_exit = false; + +class MyAuthListener : public IAuthServiceEvent { +public: + void onAuthenticationReturn(AuthResult ret) override { + std::cout << "Auth callback received!" << std::endl; + if (ret == AUTHRET_SUCCESS) { + g_authenticated = true; + } + } + // ... other required methods ... +}; + +bool AuthenticateWithMessageLoop(IAuthService* authService, const wchar_t* jwt) { + authService->SetEvent(new MyAuthListener()); + + AuthContext context; + context.jwt_token = jwt; + + if (authService->SDKAuth(context) != SDKERR_SUCCESS) { + return false; + } + + // Wait for callback with message processing + auto startTime = std::chrono::steady_clock::now(); + while (!g_authenticated && !g_exit) { + // Process Windows messages - CRITICAL! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout (30 seconds) + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsed >= 30) { + std::cerr << "Authentication timeout" << std::endl; + return false; + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} + +int main() { + // Initialize SDK + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + InitSDK(initParam); + + // Create auth service + IAuthService* authService = nullptr; + CreateAuthService(&authService); + + // Authenticate with message loop + if (!AuthenticateWithMessageLoop(authService, L"your-jwt-token")) { + std::cerr << "Authentication failed" << std::endl; + return 1; + } + + std::cout << "Authenticated successfully!" << std::endl; + + // Main application loop with message processing + while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Do other work + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +--- + +## Why SDK Documentation Doesn't Mention This + +1. **Assumed knowledge**: Windows developers are expected to know about message pumps +2. **Sample code uses it**: But in different patterns that might not be obvious +3. **Not explicitly required**: Works fine if you already have a message loop +4. **Platform-specific**: Only affects Windows SDK + +--- + +## Key Takeaways + +1. **Always process Windows messages** when waiting for SDK callbacks +2. **Use PeekMessage()** for non-blocking with timeout +3. **Use GetMessage()** for main application loop +4. **Add sleep** to avoid CPU spinning with PeekMessage() +5. **Test callbacks** to verify message loop is working +6. **This is not optional** - SDK will not work without it + +--- + +## Related Issues + +- **Authentication timeout**: Usually caused by missing message loop +- **Meeting join timeout**: Same issue +- **No video frames**: Check message loop in main application loop +- **Callbacks delayed**: Ensure message processing is frequent enough + +--- + +## See Also + +- [Authentication Flow Pattern](../examples/authentication-pattern.md) +- [Common Build Errors](build-errors.md) +- [SDK Initialization](../SKILL.md#initialization) diff --git a/partner-built/zoom-plugin/skills/oauth/RUNBOOK.md b/partner-built/zoom-plugin/skills/oauth/RUNBOOK.md new file mode 100644 index 00000000..3a14487d --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/RUNBOOK.md @@ -0,0 +1,95 @@ +# OAuth 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common OAuth failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm You Chose the Right Flow + +- S2S (`account_credentials`) for backend automation on your own account. +- User OAuth (`authorization_code`) for acting on behalf of users. +- Device flow for browserless devices. +- Client credentials for chatbot-only scenarios. + +Wrong flow choice causes scope and token errors later. + +## 2) Confirm Endpoint Split + +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +If token requests return 404/HTML, verify you are not calling `/oauth/token`. + +## 3) Confirm Redirect URI Exact Match + +- `redirect_uri` in token exchange must exactly match Marketplace config. +- Match scheme, host, path, and trailing slash. + +### State Parameter Guardrail + +- Always generate and verify `state` for user OAuth flows. +- Expire state quickly and consume once. +- If callback has `code` but state is missing/invalid, reject and restart auth. + +## 4) Confirm Scope and App Type Alignment + +- Verify required scopes are added to app. +- Re-authorize after scope changes. +- Ensure app type supports requested behavior. + +## 5) Confirm Token Lifecycle Handling + +- Access token expires ~1 hour. +- Store latest refresh token after each refresh. +- Handle refresh failure with re-auth fallback. + +### Refresh Rotation Reminder + +- Treat refresh tokens as rotating credentials. +- Persist new refresh token returned by refresh response. +- Using stale refresh tokens causes intermittent auth failures later. + +## 6) Quick Probes + +- Token endpoint returns JSON with `access_token`. +- API call to `/v2/users/me` succeeds with bearer token. +- Redirect callback receives `code` and valid `state`. + +### Copy/Paste Validation Commands + +Use these to verify OAuth plumbing in under a minute. + +```bash +# 1) S2S token request +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) User auth-code exchange +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code&code=$ZOOM_AUTH_CODE&redirect_uri=$ZOOM_REDIRECT_URI" + +# 3) Token health check +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" +``` + +## 7) Fast Decision Tree + +- **4709 redirect mismatch** -> fix exact redirect URI. +- **4702/4704 invalid client** -> wrong client credentials or app. +- **4733/4734 code errors** -> auth code expired/invalid, restart consent flow. +- **Scopes missing** -> add scopes + re-authorize. + +## 8) Flow-to-App-Type Guardrail + +- If using `account_credentials`, app must support S2S flow. +- If using `authorization_code`, app and redirect configuration must support user consent. +- If auth appears valid but API fails, verify app type, scope level, and account ownership assumptions. diff --git a/partner-built/zoom-plugin/skills/oauth/SKILL.md b/partner-built/zoom-plugin/skills/oauth/SKILL.md new file mode 100644 index 00000000..31625125 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/SKILL.md @@ -0,0 +1,901 @@ +--- +name: zoom-oauth +description: Reference skill for Zoom authentication. Use after routing to an auth workflow when choosing app credentials, grant types, scopes, token refresh behavior, or debugging Zoom OAuth failures. +user-invocable: false +triggers: + - zoom oauth + - zoom authentication + - zoom authorization + - server to server oauth + - s2s oauth + - zoom access token + - zoom refresh token + - authorization code flow + - device authorization + - pkce + - zoom api authentication + - oauth error 4709 + - oauth error 4733 + - oauth error 4735 + - redirect uri mismatch +--- + +# Zoom OAuth + +Background reference for Zoom auth and token lifecycle behavior. Prefer `setup-zoom-oauth` first, then use this skill for the exact flow, scope, and error details. + +# Zoom OAuth + +Authentication and authorization for Zoom APIs. + +## 📖 Complete Documentation + +For comprehensive guides, production patterns, and troubleshooting, see **Integrated Index section below**. + +Quick navigation: +- **[5-Minute Runbook](RUNBOOK.md)** - Preflight checks before deep debugging +- **[OAuth Flows](concepts/oauth-flows.md)** - Which flow to use and how each works +- **[Token Lifecycle](concepts/token-lifecycle.md)** - Expiration, refresh, and revocation +- **[Production Examples](examples/s2s-oauth-redis.md)** - Redis caching, MySQL storage, auto-refresh +- **[Troubleshooting](troubleshooting/common-errors.md)** - Error codes 4700-4741 + +## Prerequisites + +- Zoom app created in [Marketplace](https://marketplace.zoom.us/) +- Client ID and Client Secret +- For S2S OAuth: Account ID + +## Four Authorization Use Cases + +| Use Case | App Type | Grant Type | Industry Name | +|----------|----------|------------|---------------| +| **Account Authorization** | Server-to-Server | `account_credentials` | Client Credentials Grant, M2M, Two-legged OAuth | +| **User Authorization** | General | `authorization_code` | Authorization Code Grant, Three-legged OAuth | +| **Device Authorization** | General | `urn:ietf:params:oauth:grant-type:device_code` | Device Authorization Grant (RFC 8628) | +| **Client Authorization** | General | `client_credentials` | Client Credentials Grant (chatbot-scoped) | + +### Industry Terminology + +| Term | Meaning | +|------|---------| +| **Two-legged OAuth** | No user involved (client ↔ server) | +| **Three-legged OAuth** | User involved (user ↔ client ↔ server) | +| **M2M** | Machine-to-Machine (backend services) | +| **Public client** | Can't keep secrets (mobile, SPA) → use PKCE | +| **Confidential client** | Can keep secrets (backend servers) | +| **PKCE** | Proof Key for Code Exchange (RFC 7636), pronounced "pixy" | + +### Which Flow Should I Use? + +``` + ┌─────────────────────┐ + │ What are you │ + │ building? │ + └──────────┬──────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Backend │ │ App for other │ │ Chatbot only │ + │ automation │ │ users/accounts │ │ (Team Chat) │ + │ (your account) │ │ │ │ │ + └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + ▼ │ ▼ + ┌─────────────────┐ │ ┌─────────────────┐ + │ ACCOUNT │ │ │ CLIENT │ + │ (S2S OAuth) │ │ │ (Chatbot) │ + └─────────────────┘ │ └─────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Does device have │ + │ a browser? │ + └──────────┬──────────┘ + │ + ┌───────────────┴───────────────┐ + │ NO YES│ + ▼ ▼ + ┌─────────────────────────┐ ┌─────────────────┐ + │ DEVICE │ │ USER │ + │ (Device Flow) │ │ (Auth Code) │ + │ │ │ │ + │ Examples: │ │ + PKCE if │ + │ • Smart TV │ │ public client │ + │ • Meeting SDK device │ │ │ + └─────────────────────────┘ └─────────────────┘ +``` + +--- + +## Account Authorization (Server-to-Server OAuth) + +For backend automation without user interaction. + +### Request Access Token + +```bash +POST https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ACCOUNT_ID} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "user:read:user:admin", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh + +Access tokens expire after **1 hour**. No separate refresh flow - just request a new token. + +--- + +## User Authorization (Authorization Code Flow) + +For apps that act on behalf of users. + +### Step 1: Redirect User to Authorize + +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI} +``` + +Use `https://zoom.us/oauth/authorize` for consent, but `https://zoom.us/oauth/token` for token exchange. + +**Optional Parameters:** + +| Parameter | Description | +|-----------|-------------| +| `state` | CSRF protection, maintains state through flow | +| `code_challenge` | For PKCE (see below) | +| `code_challenge_method` | `S256` or `plain` (default: plain) | + +### Step 2: User Authorizes + +- User signs in and grants permission +- Redirects to `redirect_uri` with authorization code: + ``` + https://example.com/?code={AUTHORIZATION_CODE} + ``` + +### Step 3: Exchange Code for Token + +```bash +POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +**With PKCE:** Add `code_verifier` parameter. + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "refresh_token": "eyJ...", + "expires_in": 3600, + "scope": "user:read:user", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh Token + +```bash +POST https://zoom.us/oauth/token?grant_type=refresh_token&refresh_token={REFRESH_TOKEN} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +- Access tokens expire after **1 hour** +- Refresh token lifetime can vary; ~90 days is common for some user-based flows. Treat it as configuration/behavior that can change and rely on runtime errors + re-auth fallback. +- Always use the latest refresh token for the next request +- If refresh token expires, redirect user to authorization URL to restart flow + +### User-Level vs Account-Level Apps + +| Type | Who Can Authorize | Scope Access | +|------|-------------------|--------------| +| **User-level** | Any individual user | Scoped to themselves | +| **Account-level** | User with admin permissions | Account-wide access (admin scopes) | + +--- + +## Device Authorization (Device Flow) + +For devices without browsers (e.g., Meeting SDK apps). + +### Prerequisites + +Enable "Use App on Device" in: Features > Embed > Enable Meeting SDK + +### Step 1: Request Device Code + +```bash +POST https://zoom.us/oauth/devicecode?client_id={CLIENT_ID} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "device_code": "DEVICE_CODE", + "user_code": "abcd1234", + "verification_uri": "https://zoom.us/oauth_device", + "verification_uri_complete": "https://zoom.us/oauth/device/complete/{CODE}", + "expires_in": 900, + "interval": 5 +} +``` + +### Step 2: User Authorization + +Direct user to: +- `verification_uri` and display `user_code` for manual entry, OR +- `verification_uri_complete` (user code prefilled) + +User signs in and allows the app. + +### Step 3: Poll for Token + +Poll at the `interval` (5 seconds) until user authorizes: + +```bash +POST https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code={DEVICE_CODE} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "refresh_token": "eyJ...", + "expires_in": 3599, + "scope": "user:read:user user:read:token", + "api_url": "https://api.zoom.us" +} +``` + +### Polling Responses + +| Response | Meaning | Action | +|----------|---------|--------| +| Token returned | User authorized | Store tokens, done | +| `error: authorization_pending` | User hasn't authorized yet | Keep polling at interval | +| `error: slow_down` | Polling too fast | Increase interval by 5 seconds | +| `error: expired_token` | Device code expired (15 min) | Restart flow from Step 1 | +| `error: access_denied` | User denied authorization | Handle denial, don't retry | + +### Polling Implementation + +```javascript +async function pollForToken(deviceCode, interval) { + while (true) { + await sleep(interval * 1000); + + try { + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + return response.data; // Success - got tokens + } catch (error) { + const err = error.response?.data?.error; + if (err === 'authorization_pending') continue; + if (err === 'slow_down') { interval += 5; continue; } + throw error; // expired_token or access_denied + } + } +} +``` + +### Refresh + +Same as User Authorization. If refresh token expires, restart device flow from Step 1. + +--- + +## Client Authorization (Chatbot) + +For chatbot message operations only. + +### Request Token + +```bash +POST https://zoom.us/oauth/token?grant_type=client_credentials + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "imchat:bot", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh + +Tokens expire after **1 hour**. No refresh flow - just request a new token. + +--- + +## Using Access Tokens + +### Call API + +```bash +GET https://api.zoom.us/v2/users/me + +Headers: +Authorization: Bearer {ACCESS_TOKEN} +``` + +### Me Context + +Replace `userID` with `me` to target the token's associated user: + +| Endpoint | Methods | +|----------|---------| +| `/v2/users/me` | GET, PATCH | +| `/v2/users/me/token` | GET | +| `/v2/users/me/meetings` | GET, POST | + +--- + +## Revoke Access Token + +Works for all authorization types. + +```bash +POST https://zoom.us/oauth/revoke?token={ACCESS_TOKEN} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "status": "success" +} +``` + +--- + +## PKCE (Proof Key for Code Exchange) + +For public clients that can't securely store secrets (mobile apps, SPAs, desktop apps). + +### When to Use PKCE + +| Client Type | Use PKCE? | Why | +|-------------|-----------|-----| +| Mobile app | **Yes** | Can't securely store client secret | +| Single Page App (SPA) | **Yes** | JavaScript is visible to users | +| Desktop app | **Yes** | Binary can be decompiled | +| Meeting SDK (client-side) | **Yes** | Runs on user's device | +| Backend server | Optional | Can keep secrets, but PKCE adds security | + +### How PKCE Works + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Client │ │ Zoom │ │ Zoom │ +│ App │ │ Auth │ │ Token │ +└────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + │ 1. Generate code_verifier (random) │ │ + │ 2. Create code_challenge = SHA256(verifier) │ + │ │ │ + │ ─────── /authorize + code_challenge ──► │ │ + │ │ │ + │ ◄────── authorization_code ──────────── │ │ + │ │ │ + │ ─────────────── /token + code_verifier ─┼────────────────────────────► │ + │ │ │ + │ │ Verify: SHA256(verifier) │ + │ │ == challenge │ + │ │ │ + │ ◄───────────────────────────────────────┼─────── access_token ──────── │ + │ │ │ +``` + +### Implementation (Node.js) + +```javascript +const crypto = require('crypto'); + +function generatePKCE() { + const verifier = crypto.randomBytes(32).toString('base64url'); + const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); + return { verifier, challenge }; +} + +const pkce = generatePKCE(); + +const authUrl = `https://zoom.us/oauth/authorize?` + + `response_type=code&` + + `client_id=${CLIENT_ID}&` + + `redirect_uri=${REDIRECT_URI}&` + + `code_challenge=${pkce.challenge}&` + + `code_challenge_method=S256`; + +// Store pkce.verifier in session for callback +``` + +### Token Exchange with PKCE + +```bash +POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI}&code_verifier={VERIFIER} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +--- + +## Deauthorization + +When a user removes your app, Zoom sends a webhook to your Deauthorization Notification Endpoint URL. + +### Webhook Event + +```json +{ + "event": "app_deauthorized", + "event_ts": 1740439732278, + "payload": { + "account_id": "ACCOUNT_ID", + "user_id": "USER_ID", + "signature": "SIGNATURE", + "deauthorization_time": "2019-06-17T13:52:28.632Z", + "client_id": "CLIENT_ID" + } +} +``` + +### Requirements + +- **Delete all associated user data** after receiving this event +- **Verify webhook signature** (use secret token, verification token deprecated Oct 2023) +- Only public apps receive deauthorization webhooks (not private/dev apps) + +--- + +## Pre-Approval Flow + +Some Zoom accounts require Marketplace admin pre-approval before users can authorize apps. + +- Users can request pre-approval from their admin +- Account-level apps (admin scopes) require appropriate role permissions + +--- + +## Active Apps Notifier (AAN) + +In-meeting feature showing apps with real-time access to content. + +- Displays icon + tooltip with app info, content type being accessed, approving account +- Supported: Zoom client 5.6.7+, Meeting SDK 5.9.0+ + +--- + +## OAuth Scopes + +### Scope Types + +| Type | Description | For | +|------|-------------|-----| +| **Classic scopes** | Legacy scopes (user, admin, master levels) | Existing apps | +| **Granular scopes** | New fine-grained scopes with optional support | New apps | + +### Classic Scopes + +For previously-created apps. Three levels: +- **User-level**: Access to individual user's data +- **Admin-level**: Account-wide access, requires admin role +- **Master-level**: For master-sub account setups, requires account owner + +Full list: https://developers.zoom.us/docs/integrations/oauth-scopes/ + +### Granular Scopes + +For new apps. Format: `:::` + +| Component | Values | +|-----------|--------| +| **service** | `meeting`, `webinar`, `user`, `recording`, etc. | +| **action** | `read`, `write`, `update`, `delete` | +| **data_claim** | Data category (e.g., `participants`, `settings`) | +| **access** | empty (user), `admin`, `master` | + +Example: `meeting:read:list_meetings:admin` + +Full list: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + +### Optional Scopes + +Granular scopes can be marked as **optional** - users choose whether to grant them. + +**Basic authorization** (uses build flow defaults): +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI} +``` + +**Advanced authorization** (custom scopes per request): +``` +https://zoom.us/oauth/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={required_scopes}&optional_scope={optional_scopes} +``` + +**Include previously granted scopes:** +``` +https://zoom.us/oauth/authorize?...&include_granted_scopes&scope={additional_scopes} +``` + +### Migrating Classic to Granular + +1. Manage > select app > edit +2. Scope page > Development tab > click **Migrate** +3. Review auto-assigned granular scopes, remove unnecessary, mark optional +4. Test +5. Production tab > click **Migrate** + +**Notes:** +- No review needed if only migrating or reducing scopes +- Existing user tokens continue with classic scope values until re-authorization +- New users get granular scopes after migration + +--- + +## Common Error Codes + +| Code | Message | Solution | +|------|---------|----------| +| 4700 | Token cannot be empty | Check Authorization header has valid token | +| 4702/4704 | Invalid client | Verify Client ID and Client Secret | +| 4705 | Grant type not supported | Use: `account_credentials`, `authorization_code`, `urn:ietf:params:oauth:grant-type:device_code`, or `client_credentials` | +| 4706 | Client ID or secret missing | Add credentials to header or request params | +| 4709 | Redirect URI mismatch | Ensure redirect_uri matches app configuration exactly (including trailing slash) | +| 4711 | Refresh token invalid | Token scopes don't match client scopes | +| 4717 | App has been disabled | Contact Zoom support | +| 4733 | Code is expired | Authorization codes expire in 5 minutes - restart flow | +| 4734 | Invalid authorization code | Regenerate authorization code | +| 4735 | Owner of token does not exist | User was removed from account - re-authorize | +| 4741 | Token has been revoked | Use the most recent token from latest authorization | + +See `references/oauth-errors.md` for complete error list. + +--- + +## Quick Reference + +| Flow | Grant Type | Token Expiry | Refresh | +|------|------------|--------------|---------| +| Account (S2S) | `account_credentials` | 1 hour | Request new token | +| User | `authorization_code` | 1 hour | Use refresh_token (90 day expiry) | +| Device | `urn:ietf:params:oauth:grant-type:device_code` | 1 hour | Use refresh_token (90 day expiry) | +| Client (Chatbot) | `client_credentials` | 1 hour | Request new token | + +--- + +## Demo Guidance + +If you build an OAuth demo app, document its runtime base URL in that demo project's own +README or `.env.example`, not in this shared skill. + +## Resources + +- **OAuth docs**: https://developers.zoom.us/docs/integrations/oauth/ +- **S2S OAuth docs**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ +- **PKCE blog**: https://developers.zoom.us/blog/pcke-oauth-with-postman-rest-api/ +- **Classic scopes**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **Granular scopes**: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +## Quick Start Path + +**If you're new to Zoom OAuth, follow this order:** + +1. **Run preflight checks first** → [RUNBOOK.md](RUNBOOK.md) + +2. **Choose your OAuth flow** → [concepts/oauth-flows.md](concepts/oauth-flows.md) + - 4 flows: S2S (backend), User (SaaS), Device (no browser), Chatbot + - Decision matrix: Which flow fits your use case? + +3. **Understand token lifecycle** → [concepts/token-lifecycle.md](concepts/token-lifecycle.md) + - **CRITICAL**: How tokens expire, refresh, and revoke + - Common pitfalls: refresh token rotation + +4. **Implement your flow** → Jump to examples: + - Backend automation → [examples/s2s-oauth-redis.md](examples/s2s-oauth-redis.md) + - SaaS app → [examples/user-oauth-mysql.md](examples/user-oauth-mysql.md) + - Mobile/SPA → [examples/pkce-implementation.md](examples/pkce-implementation.md) + - Device (TV/kiosk) → [examples/device-flow.md](examples/device-flow.md) + +5. **Fix redirect URI issues** → [troubleshooting/redirect-uri-issues.md](troubleshooting/redirect-uri-issues.md) + - Most common OAuth error: Redirect URI mismatch + +6. **Implement token refresh** → [examples/token-refresh.md](examples/token-refresh.md) + - Automatic middleware pattern + - Handle refresh token rotation + +7. **Troubleshoot errors** → [troubleshooting/common-errors.md](troubleshooting/common-errors.md) + - Error code tables (4700-4741 range) + - Quick diagnostic workflow + +--- + +## Documentation Structure + +``` +oauth/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core OAuth concepts +│ ├── oauth-flows.md # 4 flows: S2S, User, Device, Chatbot +│ ├── token-lifecycle.md # Expiration, refresh, revocation +│ ├── pkce.md # PKCE security for public clients +│ ├── scopes-architecture.md # Classic vs Granular scopes +│ └── state-parameter.md # CSRF protection with state +│ +├── examples/ # Complete working code +│ ├── s2s-oauth-basic.md # S2S OAuth minimal example +│ ├── s2s-oauth-redis.md # S2S OAuth with Redis caching (production) +│ ├── user-oauth-basic.md # User OAuth minimal example +│ ├── user-oauth-mysql.md # User OAuth with MySQL + encryption (production) +│ ├── device-flow.md # Device authorization flow +│ ├── pkce-implementation.md # PKCE for SPAs/mobile apps +│ └── token-refresh.md # Auto-refresh middleware pattern +│ +├── troubleshooting/ # Problem solving guides +│ ├── common-errors.md # Error codes 4700-4741 +│ ├── redirect-uri-issues.md # Most common OAuth error +│ ├── token-issues.md # Expired, revoked, invalid tokens +│ └── scope-issues.md # Scope mismatch errors +│ +└── references/ # Reference documentation + ├── oauth-errors.md # Complete error code reference + ├── classic-scopes.md # Classic scope reference + └── granular-scopes.md # Granular scope reference +``` + +--- + +## By Use Case + +### I want to automate Zoom tasks on my own account +1. [OAuth Flows](concepts/oauth-flows.md#server-to-server-s2s-oauth) - S2S OAuth explained +2. [S2S OAuth Redis](examples/s2s-oauth-redis.md) - Production pattern with Redis caching +3. [Token Lifecycle](concepts/token-lifecycle.md) - 1hr token, no refresh + +### I want to build a SaaS app for other Zoom users +1. [OAuth Flows](concepts/oauth-flows.md#user-authorization-oauth) - User OAuth explained +2. [User OAuth MySQL](examples/user-oauth-mysql.md) - Production pattern with encryption +3. [Token Refresh](examples/token-refresh.md) - Automatic refresh middleware +4. [Redirect URI Issues](troubleshooting/redirect-uri-issues.md) - Fix most common error + +### I want to build a mobile or SPA app +1. [PKCE](concepts/pkce.md) - Why PKCE is required for public clients +2. [PKCE Implementation](examples/pkce-implementation.md) - Complete code example +3. [State Parameter](concepts/state-parameter.md) - CSRF protection + +### I want to build an app for devices without browsers (TV, kiosk) +1. [OAuth Flows](concepts/oauth-flows.md#device-authorization-flow) - Device flow explained +2. [Device Flow Example](examples/device-flow.md) - Complete polling implementation +3. [Common Errors](troubleshooting/common-errors.md) - Device-specific errors + +### I'm building a Team Chat bot +1. [OAuth Flows](concepts/oauth-flows.md#client-authorization-chatbot) - Chatbot flow explained +2. [S2S OAuth Basic](examples/s2s-oauth-basic.md) - Similar pattern, different grant type +3. [Scopes Architecture](concepts/scopes-architecture.md) - Chatbot-specific scopes + +### I'm getting redirect URI errors (4709) +1. [Redirect URI Issues](troubleshooting/redirect-uri-issues.md) - **START HERE!** +2. [Common Errors](troubleshooting/common-errors.md#4709-redirect-uri-mismatch) - Error details +3. [User OAuth Basic](examples/user-oauth-basic.md) - See correct pattern + +### I'm getting token errors (4700-4741) +1. [Token Issues](troubleshooting/token-issues.md) - Diagnostic workflow +2. [Token Lifecycle](concepts/token-lifecycle.md) - Understand expiration +3. [Token Refresh](examples/token-refresh.md) - Implement auto-refresh +4. [Common Errors](troubleshooting/common-errors.md) - Error code tables + +### I'm getting scope errors (4711) +1. [Scope Issues](troubleshooting/scope-issues.md) - Mismatch causes +2. [Scopes Architecture](concepts/scopes-architecture.md) - Classic vs Granular +3. [Classic Scopes](references/classic-scopes.md) - Complete scope reference +4. [Granular Scopes](references/granular-scopes.md) - Granular scope reference + +### I need to refresh tokens +1. [Token Lifecycle](concepts/token-lifecycle.md#refresh-strategy) - When to refresh +2. [Token Refresh](examples/token-refresh.md) - Middleware pattern +3. [Token Issues](troubleshooting/token-issues.md#refresh-token-problems) - Common mistakes + +### I want to understand the difference between Classic and Granular scopes +1. [Scopes Architecture](concepts/scopes-architecture.md) - **Complete comparison** +2. [Classic Scopes](references/classic-scopes.md) - `resource:level` format +3. [Granular Scopes](references/granular-scopes.md) - `service:action:data_claim:access` format + +### I need to secure my OAuth implementation +1. [PKCE](concepts/pkce.md) - Public client security +2. [State Parameter](concepts/state-parameter.md) - CSRF protection +3. [User OAuth MySQL](examples/user-oauth-mysql.md#token-encryption) - Token encryption at rest + +### I want to migrate from JWT app to S2S OAuth +1. [S2S OAuth Redis](examples/s2s-oauth-redis.md) - Modern replacement +2. [Token Lifecycle](concepts/token-lifecycle.md) - Different token behavior + +> **Note**: JWT App Type was deprecated in June 2023. Migrate to S2S OAuth for server-to-server automation. + +--- + +## Most Critical Documents + +### 1. OAuth Flows (DECISION DOCUMENT) +**[concepts/oauth-flows.md](concepts/oauth-flows.md)** + +Understand which of the 4 flows to use: +- **S2S OAuth**: Backend automation (your account) +- **User OAuth**: SaaS apps (users authorize you) +- **Device Flow**: Devices without browsers +- **Chatbot**: Team Chat bots only + +### 2. Token Lifecycle (MOST COMMON ISSUE) +**[concepts/token-lifecycle.md](concepts/token-lifecycle.md)** + +99% of OAuth issues stem from misunderstanding: +- Token expiration (1 hour for all flows) +- Refresh token rotation (must save new refresh token) +- Revocation behavior (invalidates all tokens) + +### 3. Redirect URI Issues (MOST COMMON ERROR) +**[troubleshooting/redirect-uri-issues.md](troubleshooting/redirect-uri-issues.md)** + +Error 4709 ("Redirect URI mismatch") is the #1 OAuth error. +Must match EXACTLY (including trailing slash, http vs https). + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Refresh Token Rotation** + - Each refresh returns a NEW refresh token + - Old refresh token becomes invalid + - Failure to save new token causes 4735 errors + - See: [Token Refresh](examples/token-refresh.md) + +2. **S2S OAuth Uses Redis, User OAuth Uses Database** + - S2S: Single token for entire account → Redis (ephemeral) + - User: Per-user tokens → Database (persistent) + - See: [S2S OAuth Redis](examples/s2s-oauth-redis.md) vs [User OAuth MySQL](examples/user-oauth-mysql.md) + +3. **Redirect URI Must Match EXACTLY** + - Trailing slash matters: `/callback` ≠ `/callback/` + - Protocol matters: `http://` ≠ `https://` + - Port matters: `:3000` ≠ `:3001` + - See: [Redirect URI Issues](troubleshooting/redirect-uri-issues.md) + +4. **PKCE Required for Public Clients** + - Mobile apps CANNOT keep secrets + - SPAs CANNOT keep secrets + - PKCE prevents authorization code interception + - See: [PKCE](concepts/pkce.md) + +5. **State Parameter Prevents CSRF** + - Generate random state before redirect + - Store in session + - Verify on callback + - See: [State Parameter](concepts/state-parameter.md) + +6. **Token Storage Must Be Encrypted** + - NEVER store tokens in plain text + - Use AES-256 minimum + - See: [User OAuth MySQL](examples/user-oauth-mysql.md#token-encryption) + +7. **JWT App Type is Deprecated (June 2023)** + - No new JWT apps can be created + - Existing apps still work but will eventually be sunset + - Migrate to S2S OAuth or User OAuth + +8. **Scope Levels Determine Authorization Requirements** + - No suffix (user-level): Any user can authorize + - `:admin`: Requires admin role + - `:master`: Requires account owner (multi-account) + - See: [Scopes Architecture](concepts/scopes-architecture.md) + +9. **Authorization Codes Expire in 5 Minutes** + - Exchange code for token immediately + - Don't cache authorization codes + - See: [Token Lifecycle](concepts/token-lifecycle.md#authorization-code-expiration) + +10. **Device Flow Requires Polling** + - Poll at interval returned by `/devicecode` (usually 5s) + - Handle `authorization_pending`, `slow_down`, `expired_token` + - See: [Device Flow](examples/device-flow.md) + +--- + +## Quick Reference + +### "Which OAuth flow should I use?" +→ [OAuth Flows](concepts/oauth-flows.md) + +### "Redirect URI mismatch error (4709)" +→ [Redirect URI Issues](troubleshooting/redirect-uri-issues.md) + +### "Token expired or invalid" +→ [Token Issues](troubleshooting/token-issues.md) + +### "Refresh token invalid (4735)" +→ [Token Refresh](examples/token-refresh.md) - Must save new refresh token + +### "Scope mismatch error (4711)" +→ [Scope Issues](troubleshooting/scope-issues.md) + +### "How do I secure my OAuth app?" +→ [PKCE](concepts/pkce.md) + [State Parameter](concepts/state-parameter.md) + +### "How do I implement auto-refresh?" +→ [Token Refresh](examples/token-refresh.md) + +### "What's the difference between Classic and Granular scopes?" +→ [Scopes Architecture](concepts/scopes-architecture.md) + +### "What error code means what?" +→ [Common Errors](troubleshooting/common-errors.md) + +--- + +## Document Version + +Based on **Zoom OAuth API v2** (2024+) + +**Deprecated:** JWT App Type (June 2023) + +--- + +**Happy coding!** + +Remember: Start with [OAuth Flows](concepts/oauth-flows.md) to understand which flow fits your use case! + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/oauth/concepts/oauth-flows.md b/partner-built/zoom-plugin/skills/oauth/concepts/oauth-flows.md new file mode 100644 index 00000000..9c03d644 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/concepts/oauth-flows.md @@ -0,0 +1,530 @@ +# Zoom OAuth Flows + +Zoom supports 4 OAuth 2.0 flows. This guide helps you choose the right one and understand how each works. + +Endpoint split to remember: +- Authorization URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +## Quick Decision Matrix + +| Your Scenario | Flow | Grant Type | +|---------------|------|------------| +| Backend automation on **your own account** | S2S OAuth | `account_credentials` | +| SaaS app for **other Zoom users** | User OAuth | `authorization_code` | +| Device without browser (TV, kiosk, IoT) | Device Flow | `urn:ietf:params:oauth:grant-type:device_code` | +| **Team Chat bot only** | Chatbot | `client_credentials` | + +## Two-Legged vs Three-Legged + +| Type | User Involved? | Zoom Flows | +|------|----------------|------------| +| **Two-legged** | No (app acts on its own) | S2S OAuth, Chatbot | +| **Three-legged** | Yes (user authorizes app) | User OAuth, Device Flow | + +--- + +## 1. Server-to-Server (S2S) OAuth + +**When to use:** +- Backend automation on your own Zoom account +- No end-user interaction needed +- Account-wide API access + +**Grant type:** `account_credentials` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: None (request new token when expired) + +**Credentials required:** +- Account ID +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌──────────────┐ ┌──────────────┐ +│ Your App │ │ Zoom OAuth │ +│ (Backend) │ │ Server │ +└──────┬───────┘ └──────┬───────┘ + │ │ + │ POST /oauth/token │ + │ grant_type=account_credentials │ + │ account_id={ACCOUNT_ID} │ + │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │──────────────────────────────────────────────────>│ + │ │ + │ │ Validate + │ │ credentials + │ │ + │ { access_token, expires_in, scope } │ + │<──────────────────────────────────────────────────│ + │ │ + │ API Requests with Bearer token │ + │ (valid for 1 hour) │ + │ │ +``` + +### Implementation + +```javascript +const axios = require('axios'); +const qs = require('query-string'); + +const getToken = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope, token_type } +}; +``` + +### Key Points + +✅ **Simple:** No redirect URIs, no user interaction +✅ **Secure:** Credentials stored server-side only +✅ **Account-wide:** Single token for all account operations +⚠️ **No refresh token:** Just request a new token when expired (cache with TTL) + +--- + +## 2. User Authorization OAuth + +**When to use:** +- Building a SaaS app for other Zoom users +- Users authorize your app to act on their behalf +- Need per-user access control + +**Grant type:** `authorization_code` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior) + +**Credentials required:** +- Client ID +- Client Secret +- Redirect URI (must match marketplace app config) + +### Flow Diagram + +``` +┌────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ User │ │ Your App │ │ Zoom OAuth │ │ Zoom API │ +│Browser │ │ (Server) │ │ Server │ │ Server │ +└────┬───┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ │ + │ 1. Click "Add App" │ │ │ + │─────────────────────>│ │ │ + │ │ │ │ + │ 2. Redirect to authorize │ │ + │https://zoom.us/oauth/authorize? │ │ + │ client_id={ID} │ │ + │ redirect_uri={URI} │ │ + │ response_type=code │ │ + │ state={RANDOM} │ │ + │<─────────────────────│ │ │ + │ │ │ │ + │ 3. User sees "Allow" page │ │ + │─────────────────────────────────────────────────>│ │ + │ │ │ │ + │ 4. User clicks "Allow" │ │ + │─────────────────────────────────────────────────>│ │ + │ │ │ │ + │ 5. Redirect to callback │ │ + │ {REDIRECT_URI}?code={CODE}&state={STATE} │ │ + │<─────────────────────────────────────────────────│ │ + │ │ │ │ + │ 6. Send code to app │ │ │ + │─────────────────────>│ │ │ + │ │ │ │ + │ │ 7. Exchange code for token │ + │ │ POST /oauth/token │ + │ │ grant_type=authorization_code │ + │ │ code={CODE} │ + │ │ redirect_uri={URI} │ + │ │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │ │─────────────────────────────────────────────────────>│ + │ │ │ │ + │ │ 8. Return tokens │ │ + │ │ { access_token, refresh_token, expires_in } │ + │ │<─────────────────────────────────────────────────────│ + │ │ │ │ + │ │ 9. Store tokens (encrypted) │ + │ │ per user │ │ + │ │ │ │ + │ │ 10. API requests │ + │ │ Authorization: Bearer {ACCESS_TOKEN} │ + │ │─────────────────────────────────────────────────────────────────>│ +``` + +### Implementation + +#### Step 1: Redirect to Authorization + +```javascript +const express = require('express'); +const crypto = require('crypto'); + +app.get('/auth', (req, res) => { + const state = crypto.randomBytes(16).toString('hex'); + req.session.oauthState = state; // Store for verification + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('state', state); + + res.redirect(authURL.toString()); +}); +``` + +#### Step 2: Handle Callback and Exchange Code + +```javascript +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + // Verify state to prevent CSRF + if (state !== req.session.oauthState) { + return res.status(403).send('Invalid state parameter'); + } + + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.ZOOM_REDIRECT_URL + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token, refresh_token } = response.data; + + // Store tokens securely (encrypted) per user + await saveUserTokens(req.session.userId, { + access_token, + refresh_token + }); + + res.send('Authorization successful!'); + } catch (error) { + res.status(500).send('Token exchange failed'); + } +}); +``` + +### Key Points + +✅ **User-controlled:** Users authorize access to their own account +✅ **Per-user tokens:** Each user gets their own access/refresh tokens +✅ **Refresh support:** Tokens can be refreshed while the refresh token remains valid (lifetime varies; ~90 days is common) +⚠️ **Redirect URI must match exactly:** Including trailing slash, protocol, port +⚠️ **State parameter required:** Prevent CSRF attacks +⚠️ **Authorization code expires in 5 minutes:** Exchange immediately + +--- + +## 3. Device Authorization Flow + +**When to use:** +- Devices without a browser (smart TVs, kiosks, IoT devices) +- Devices with limited input capabilities +- User authorizes on a separate device (phone/computer) + +**Grant type:** `urn:ietf:params:oauth:grant-type:device_code` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior) + +**Credentials required:** +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌────────────┐ ┌──────────────┐ ┌────────────┐ +│ Device │ │ Zoom OAuth │ │User's Phone│ +│ (TV/Kiosk) │ │ Server │ │ / Computer │ +└──────┬─────┘ └──────┬───────┘ └─────┬──────┘ + │ │ │ + │ 1. POST /oauth/devicecode │ + │ client_id={CLIENT_ID} │ + │───────────────────────>│ │ + │ │ │ + │ 2. Return device_code, user_code, verification_uri, interval + │ { device_code, user_code, verification_uri, interval } + │<───────────────────────│ │ + │ │ │ + │ 3. Display to user: │ │ + │ "Go to zoom.us/activate" │ + │ "Enter code: ABC-DEF" │ │ + │ │ │ + │ │ 4. User visits URL │ + │ │ and enters user_code │ + │ │<────────────────────────│ + │ │ │ + │ │ 5. User clicks "Allow" │ + │ │<────────────────────────│ + │ │ │ + │ 6. Poll for token (every {interval} seconds) │ + │ POST /oauth/token │ + │ grant_type=urn:ietf:params:oauth:grant-type:device_code + │ device_code={DEVICE_CODE} │ + │───────────────────────>│ │ + │ │ │ + │ 7. Response (repeat until success or timeout) │ + │ - authorization_pending (keep polling) │ + │ - slow_down (increase interval) │ + │ - expired_token (restart flow) │ + │ - { access_token, refresh_token } (success!) │ + │<───────────────────────│ │ +``` + +### Implementation + +#### Step 1: Request Device Code + +```javascript +const requestDeviceCode = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/devicecode', + qs.stringify({ + client_id: process.env.ZOOM_CLIENT_ID + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; + /* + { + device_code: "GmRhmhcxhwAzkoEqiMEg_DnyEysNmsh6JCl-fNkAghaUg", + user_code: "ABC-DEF", + verification_uri: "https://zoom.us/activate", + expires_in: 900, // 15 minutes + interval: 5 // Poll every 5 seconds + } + */ +}; +``` + +#### Step 2: Display User Code + +```javascript +const { device_code, user_code, verification_uri, interval } = await requestDeviceCode(); + +console.log(`\nGo to: ${verification_uri}`); +console.log(`Enter code: ${user_code}\n`); +``` + +#### Step 3: Poll for Token + +```javascript +const pollForToken = async (device_code, interval) => { + const pollInterval = interval * 1000; // Convert to milliseconds + let currentInterval = pollInterval; + + return new Promise((resolve, reject) => { + const poll = async () => { + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: device_code + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + // Success! Got tokens + resolve(response.data); + + } catch (error) { + const errorCode = error.response?.data?.error; + + if (errorCode === 'authorization_pending') { + // User hasn't authorized yet, keep polling + setTimeout(poll, currentInterval); + + } else if (errorCode === 'slow_down') { + // Zoom wants us to slow down, increase interval by 5s + currentInterval += 5000; + setTimeout(poll, currentInterval); + + } else if (errorCode === 'expired_token') { + // Device code expired (15 minutes), restart flow + reject(new Error('Device code expired. Please restart authorization.')); + + } else { + // Other error + reject(error); + } + } + }; + + // Start polling + poll(); + }); +}; +``` + +### Key Points + +✅ **No browser required:** User authorizes on separate device +✅ **Simple user experience:** Just enter a short code +✅ **Polling-based:** Device polls until user authorizes +⚠️ **Must enable in app settings:** "Use App on Device" feature flag +⚠️ **Device code expires in 15 minutes:** User must complete authorization quickly +⚠️ **Respect polling interval:** Returned by `/devicecode` endpoint (usually 5s) +⚠️ **Handle slow_down:** Increase interval by 5s when requested + +--- + +## 4. Client Authorization (Chatbot) + +**When to use:** +- Building a Team Chat bot ONLY +- App needs `imchat:bot` scope +- Simpler than S2S OAuth + +**Grant type:** `client_credentials` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: None (request new token when expired) + +**Credentials required:** +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌──────────────┐ ┌──────────────┐ +│ Chatbot App │ │ Zoom OAuth │ +│ (Backend) │ │ Server │ +└──────┬───────┘ └──────┬───────┘ + │ │ + │ POST /oauth/token │ + │ grant_type=client_credentials │ + │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │──────────────────────────────────────────────────>│ + │ │ + │ { access_token, expires_in, scope } │ + │<──────────────────────────────────────────────────│ + │ │ + │ Chatbot API Requests with Bearer token │ + │ (valid for 1 hour) │ + │ │ +``` + +### Implementation + +```javascript +const getChatbotToken = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'client_credentials' + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope, token_type } +}; +``` + +### Key Points + +✅ **Simplest flow:** Just request token with credentials +✅ **Chatbot-specific:** Limited to Team Chat bot operations +⚠️ **No refresh token:** Request new token when expired +⚠️ **Scope limited:** Primarily `imchat:bot` scope + +--- + +## Comparison Table + +| Feature | S2S OAuth | User OAuth | Device Flow | Chatbot | +|---------|-----------|------------|-------------|---------| +| **Grant Type** | `account_credentials` | `authorization_code` | `device_code` | `client_credentials` | +| **User Interaction** | No | Yes (browser) | Yes (separate device) | No | +| **Access Token Lifetime** | 1 hour | 1 hour | 1 hour | 1 hour | +| **Refresh Token** | ❌ None | ✅ ~90 days (commonly) | ✅ ~90 days (commonly) | ❌ None | +| **Redirect URI** | ❌ Not needed | ✅ Required | ❌ Not needed | ❌ Not needed | +| **PKCE Support** | ❌ N/A | ✅ Optional | ❌ N/A | ❌ N/A | +| **State Parameter** | ❌ N/A | ✅ Recommended | ❌ N/A | ❌ N/A | +| **Account Access** | Account-wide | Per-user | Per-user | Account-wide | +| **Token Storage** | Redis (ephemeral) | Database (persistent) | Database (persistent) | Redis (ephemeral) | +| **Use Case** | Backend automation | SaaS apps | TV/kiosk apps | Chat bots | + +--- + +## OAuth 2.0 Standards + +Zoom OAuth follows these RFCs: + +- **RFC 6749**: OAuth 2.0 Authorization Framework + - https://datatracker.ietf.org/doc/html/rfc6749 + +- **RFC 7636**: PKCE (Proof Key for Code Exchange) + - https://datatracker.ietf.org/doc/html/rfc7636 + +- **RFC 8628**: Device Authorization Grant + - https://datatracker.ietf.org/doc/html/rfc8628 + +--- + +## Next Steps + +- **Understand token lifecycle** → [token-lifecycle.md](token-lifecycle.md) +- **Learn about PKCE** → [pkce.md](pkce.md) +- **Implement your flow:** + - S2S → [../examples/s2s-oauth-redis.md](../examples/s2s-oauth-redis.md) + - User → [../examples/user-oauth-mysql.md](../examples/user-oauth-mysql.md) + - Device → [../examples/device-flow.md](../examples/device-flow.md) diff --git a/partner-built/zoom-plugin/skills/oauth/concepts/pkce.md b/partner-built/zoom-plugin/skills/oauth/concepts/pkce.md new file mode 100644 index 00000000..8e5e17c8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/concepts/pkce.md @@ -0,0 +1,415 @@ +# PKCE (Proof Key for Code Exchange) + +PKCE (pronounced "pixy") is a security extension to OAuth 2.0 for public clients that cannot securely store a client secret. + +## When PKCE is Required + +| Client Type | Can Store Secrets? | PKCE Required? | Examples | +|-------------|-------------------|----------------|----------| +| **Confidential** | ✅ Yes (server-side) | ❌ Optional | Backend servers, traditional web apps | +| **Public** | ❌ No (client-side) | ✅ **Required** | Mobile apps, SPAs, desktop apps | + +###Why Public Clients Can't Keep Secrets + +```javascript +// ❌ INSECURE: Client secret embedded in mobile/SPA code +const CLIENT_SECRET = "abc123"; // Anyone can decompile/inspect and find this! + +// Attacker can: +// 1. Extract CLIENT_SECRET from app +// 2. Intercept authorization code +// 3. Exchange code for tokens using stolen secret +``` + +## How PKCE Works + +PKCE prevents authorization code interception attacks **without requiring a client secret**. + +### Flow Diagram + +``` +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Mobile App │ │ Zoom OAuth │ │ Attacker │ +│ (Public) │ │ Server │ │ (Intercepting│ +└──────┬──────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + │ 1. Generate code_verifier │ │ + │ (random 43-128 chars) │ │ + │ │ │ + │ 2. Create code_challenge │ │ + │ SHA256(code_verifier) │ │ + │ │ │ + │ 3. Authorize with challenge │ │ + │https://zoom.us/oauth/authorize?│ │ + │ code_challenge={HASH} │ │ + │ code_challenge_method=S256 │ │ + │──────────────────────────────>│ │ + │ │ │ + │ 4. User authorizes │ │ + │──────────────────────────────>│ │ + │ │ │ + │ 5. Return authorization code │ │ + │<──────────────────────────────│ │ + │ │ │ + │ │ Attacker intercepts code │ + │ │<───────────────────────────────│ + │ │ │ + │ │ Attacker tries to exchange │ + │ │ (but doesn't have verifier!) │ + │ │<───────────────────────────────│ + │ │ │ + │ │ REJECTED: Missing verifier │ + │ │───────────────────────────────>│ + │ │ │ + │ 6. Exchange code with verifier│ │ + │ POST /oauth/token │ │ + │ code={CODE} │ │ + │ code_verifier={ORIGINAL} │ │ + │──────────────────────────────>│ │ + │ │ │ + │ │ Verify: │ + │ │ SHA256(code_verifier) │ + │ │ == code_challenge? │ + │ │ │ + │ 7. Return tokens │ │ + │<──────────────────────────────│ │ + │ │ │ +``` + +### Key Concept + +- **code_verifier:** Random secret generated by app (kept secret) +- **code_challenge:** SHA256 hash of code_verifier (sent to Zoom) +- **Zoom stores challenge:** During authorization +- **App proves possession:** By providing original verifier during token exchange +- **Attacker fails:** Even with intercepted code, can't generate matching verifier + +--- + +## Implementation + +### Step 1: Generate PKCE Parameters + +```javascript +const crypto = require('crypto'); + +function generatePKCE() { + // Generate random code_verifier (43-128 characters) + const verifier = crypto + .randomBytes(32) + .toString('base64url'); // base64url encoding (no padding) + + // Create code_challenge: SHA256(code_verifier) + const challenge = crypto + .createHash('sha256') + .update(verifier) + .digest('base64url'); + + return { + code_verifier: verifier, + code_challenge: challenge + }; +} + +// Example output: +// { +// code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", +// code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" +// } +``` + +### Step 2: Store code_verifier Securely + +```javascript +// Store in session (server-side) or secure storage (mobile) +req.session.pkce_verifier = code_verifier; + +// For mobile apps, use secure storage: +// - iOS: Keychain +// - Android: EncryptedSharedPreferences +``` + +### Step 3: Redirect to Authorization with code_challenge + +```javascript +app.get('/auth', (req, res) => { + const { code_verifier, code_challenge } = generatePKCE(); + + // Store verifier for later (Step 5) + req.session.pkce_verifier = code_verifier; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('code_challenge', code_challenge); + authURL.searchParams.set('code_challenge_method', 'S256'); // SHA256 + + res.redirect(authURL.toString()); +}); +``` + +### Step 4: Exchange Code with code_verifier + +```javascript +app.get('/callback', async (req, res) => { + const { code } = req.query; + const code_verifier = req.session.pkce_verifier; // Retrieve stored verifier + + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.ZOOM_REDIRECT_URL, + code_verifier: code_verifier // Prove possession of original verifier + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token, refresh_token } = response.data; + + // Success! Store tokens + await saveTokens({ access_token, refresh_token }); + + // Clean up verifier + delete req.session.pkce_verifier; + + res.send('Authorization successful!'); + } catch (error) { + res.status(500).send('Token exchange failed'); + } +}); +``` + +--- + +## PKCE Methods + +Zoom supports two code_challenge_method values: + +| Method | Description | Security | Support | +|--------|-------------|----------|---------| +| **S256** | SHA256 hash of verifier | ✅ **Recommended** | All OAuth 2.0 servers | +| **plain** | Verifier sent as-is (no hash) | ⚠️ Weaker | Legacy support only | + +**Always use S256:** + +```javascript +// ✅ RECOMMENDED +authURL.searchParams.set('code_challenge_method', 'S256'); +``` + +```javascript +// ❌ AVOID (less secure) +authURL.searchParams.set('code_challenge_method', 'plain'); +``` + +--- + +## Security Benefits + +### Without PKCE (Vulnerable) + +``` +Attacker intercepts authorization code + ↓ +Attacker exchanges code with client_secret + ↓ +Attacker gets access_token and refresh_token + ↓ +Attacker has full account access +``` + +### With PKCE (Protected) + +``` +Attacker intercepts authorization code + ↓ +Attacker tries to exchange code + ↓ +Zoom: "Provide code_verifier" + ↓ +Attacker doesn't have original verifier + ↓ +Token exchange FAILS + ↓ +Legitimate app exchanges with correct verifier + ↓ +Legitimate app gets tokens +``` + +--- + +## Common Mistakes + +### 1. Not Storing code_verifier + +```javascript +// ❌ WRONG: Generating new verifier on callback +app.get('/callback', async (req, res) => { + const { code_challenge } = generatePKCE(); // New verifier! + // This won't match the original challenge +}); +``` + +```javascript +// ✅ CORRECT: Retrieve stored verifier +app.get('/callback', async (req, res) => { + const code_verifier = req.session.pkce_verifier; // Original verifier +}); +``` + +### 2. Using 'plain' Method + +```javascript +// ❌ AVOID +code_challenge_method: 'plain' // Less secure +``` + +```javascript +// ✅ RECOMMENDED +code_challenge_method: 'S256' // SHA256 hashing +``` + +### 3. Exposing code_verifier + +```javascript +// ❌ WRONG: Including verifier in URL +authURL.searchParams.set('code_verifier', verifier); // Don't send verifier during auth! +``` + +```javascript +// ✅ CORRECT: Only send code_challenge +authURL.searchParams.set('code_challenge', challenge); +authURL.searchParams.set('code_challenge_method', 'S256'); +``` + +--- + +## Mobile App Implementation + +### iOS (Swift) + +```swift +import CryptoKit + +func generatePKCE() -> (verifier: String, challenge: String) { + // Generate random verifier + var buffer = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + let verifier = Data(buffer).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + // Create SHA256 challenge + let data = verifier.data(using: .utf8)! + let hash = SHA256.hash(data: data) + let challenge = Data(hash).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + return (verifier, challenge) +} + +// Store verifier in Keychain +KeychainWrapper.standard.set(verifier, forKey: "pkce_verifier") +``` + +### Android (Kotlin) + +```kotlin +import java.security.MessageDigest +import java.security.SecureRandom +import android.util.Base64 + +fun generatePKCE(): Pair { + // Generate random verifier + val bytes = ByteArray(32) + SecureRandom().nextBytes(bytes) + val verifier = Base64.encodeToString(bytes, + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + + // Create SHA256 challenge + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(verifier.toByteArray()) + val challenge = Base64.encodeToString(hash, + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + + return Pair(verifier, challenge) +} + +// Store verifier in EncryptedSharedPreferences +val encryptedPrefs = EncryptedSharedPreferences.create(...) +encryptedPrefs.edit().putString("pkce_verifier", verifier).apply() +``` + +--- + +## Testing PKCE Implementation + +### 1. Verify code_challenge Format + +```javascript +const { code_verifier, code_challenge } = generatePKCE(); + +console.log('Verifier length:', code_verifier.length); // Should be 43-128 +console.log('Challenge length:', code_challenge.length); // Should be 43 for S256 +console.log('Verifier chars:', /^[A-Za-z0-9_-]+$/.test(code_verifier)); // true +console.log('Challenge chars:', /^[A-Za-z0-9_-]+$/.test(code_challenge)); // true +``` + +### 2. Verify SHA256 Hashing + +```javascript +const crypto = require('crypto'); + +const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; +const expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; + +const computed_challenge = crypto + .createHash('sha256') + .update(verifier) + .digest('base64url'); + +console.log(computed_challenge === expected_challenge); // Should be true +``` + +### 3. Test Token Exchange + +```bash +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" \ + -d "code=YOUR_AUTH_CODE" \ + -d "redirect_uri=YOUR_REDIRECT_URI" \ + -d "code_verifier=YOUR_CODE_VERIFIER" + +# Should return { access_token, refresh_token, ... } +``` + +--- + +## PKCE Specification + +PKCE follows **RFC 7636**: +- https://datatracker.ietf.org/doc/html/rfc7636 + +--- + +## Next Steps + +- **Implement PKCE in your app** → [../examples/pkce-implementation.md](../examples/pkce-implementation.md) +- **Add state parameter** → [state-parameter.md](state-parameter.md) +- **Understand OAuth flows** → [oauth-flows.md](oauth-flows.md) diff --git a/partner-built/zoom-plugin/skills/oauth/concepts/scopes-architecture.md b/partner-built/zoom-plugin/skills/oauth/concepts/scopes-architecture.md new file mode 100644 index 00000000..e1bc4bdc --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/concepts/scopes-architecture.md @@ -0,0 +1,244 @@ +# Scopes Architecture + +Zoom OAuth uses scopes to limit API access. Understanding Classic vs Granular scopes is critical. + +## Scope Types + +| Type | Format | Example | Status | +|------|--------|---------|--------| +| **Classic** | `resource:level` | `meeting:write:admin` | Active | +| **Granular** | `service:action:data_claim:access` | `meeting:write:meeting:admin` | Active (newer) | + +## Classic Scopes + +### Format + +``` +{resource}:{action}:{level} +``` + +**Examples:** +- `meeting:read` - Read user's own meetings +- `meeting:write:admin` - Create/update meetings for all account users +- `recording:read:master` - Read recordings across all sub-accounts + +### Scope Levels + +| Level | Access | Who Can Authorize | Example | +|-------|--------|-------------------|---------| +| **(none)** | Own data only | Any user | `meeting:read` | +| `:admin` | Account-wide | Admin role required | `meeting:write:admin` | +| `:master` | Multi-account | Account owner only | `user:master` | + +### Common Classic Scopes + +``` +meeting:read # View own meetings +meeting:write # Create/edit own meetings +meeting:write:admin # Manage all account meetings + +user:read # View own profile +user:write:admin # Manage account users + +recording:read # View own recordings +recording:write:admin # Manage account recordings + +webinar:read # View own webinars +webinar:write:admin # Manage account webinars + +imchat:bot # Team Chat bot access +``` + +## Granular Scopes + +### Format + +``` +{service}:{action}:{data_claim}:{access_level} +``` + +**Examples:** +- `meeting:read:meeting:user` - Read user's own meetings +- `meeting:write:invite_links:admin` - Create invite links for account +- `recording:delete:recording_file:admin` - Delete recording files account-wide + +### Components + +1. **service**: API category (meeting, user, recording, etc.) +2. **action**: Operation (read, write, delete, etc.) +3. **data_claim**: Specific data type (meeting, participant, invite_links, etc.) +4. **access_level**: Scope of access (user, admin, account, etc.) + +### Access Levels (Granular) + +| Level | Access | Example | +|-------|--------|---------| +| `user` | Own data | `meeting:read:meeting:user` | +| `admin` | Account-wide | `meeting:write:meeting:admin` | +| `account` | Account settings | `account:read:settings:account` | + +## Classic vs Granular Comparison + +### Meetings Scope Example + +| Classic | Granular Equivalent | +|---------|---------------------| +| `meeting:read` | `meeting:read:meeting:user` + `meeting:read:list_meetings:user` | +| `meeting:write:admin` | `meeting:write:meeting:admin` + `meeting:write:settings:admin` + more | + +### Why Granular Scopes Exist + +**Classic scopes** are broad: +- `meeting:write:admin` grants ALL meeting write permissions account-wide +- Includes create, update, delete, settings, etc. + +**Granular scopes** are specific: +- `meeting:write:meeting:admin` - Only create/update meetings +- `meeting:delete:meeting:admin` - Only delete meetings +- `meeting:write:settings:admin` - Only update settings + +**Principle of Least Privilege:** Request only the granular scopes you need. + +## Choosing Between Classic and Granular + +### Use Classic When: +- You need broad access (e.g., full meeting management) +- Simpler scope management preferred +- Legacy app migration + +### Use Granular When: +- You need specific permissions only +- Implementing principle of least privilege +- Building security-sensitive apps + +### Can You Mix? + +✅ **Yes**, you can request both Classic and Granular scopes in the same app. + +``` +scope=meeting:read user:write:admin meeting:write:invite_links:admin +``` + +## Requesting Scopes + +### During App Creation (S2S OAuth, Chatbot) + +Scopes are configured in Zoom Marketplace: +1. Go to your app in https://marketplace.zoom.us +2. Click "Scopes" tab +3. Select required scopes (Classic or Granular) +4. Click "Continue" + +Token will include all configured scopes. + +### During Authorization (User OAuth, Device Flow) + +Scopes are requested in authorization URL: + +```javascript +const authURL = new URL('https://zoom.us/oauth/authorize'); +authURL.searchParams.set('response_type', 'code'); +authURL.searchParams.set('client_id', CLIENT_ID); +authURL.searchParams.set('redirect_uri', REDIRECT_URI); + +// Request specific scopes (space-separated) +authURL.searchParams.set('scope', 'meeting:read user:read recording:read'); + +// User sees consent screen listing these scopes +``` + +## Scope Consent Screen + +When user authorizes your app, they see: + +``` +[Your App Name] wants to: + +✓ View your meetings (meeting:read) +✓ View your profile (user:read) +✓ View your recordings (recording:read) + +[Deny] [Authorize] +``` + +## Scope Errors + +### Error 4711: Scope Mismatch + +**Cause:** Token's scopes don't include required scope for API endpoint. + +**Example:** +```javascript +// Token has: meeting:read +// API requires: meeting:write +await axios.post('https://api.zoom.us/v2/users/me/meetings', {...}, { + headers: { Authorization: `Bearer ${token}` } +}); +// Error 4711: Insufficient scope +``` + +**Solution:** +1. Add required scope in Zoom Marketplace (S2S/Chatbot) +2. OR request scope in authorization URL (User/Device) +3. Re-authorize user to grant new scopes + +## Checking Token Scopes + +### Decode Access Token Scopes + +```javascript +// S2S OAuth: scopes returned in token response +const { access_token, scope } = tokenResponse.data; +console.log('Scopes:', scope); // "meeting:read user:read recording:write" + +// User OAuth: scopes returned during token exchange +const { access_token, scope } = tokenResponse.data; +console.log('Granted scopes:', scope.split(' ')); // ['meeting:read', 'user:read', ...] +``` + +### API to Get Token Scopes + +```bash +curl -H "Authorization: Bearer {access_token}" \ + https://zoom.us/oauth/token +``` + +## Best Practices + +### 1. Request Minimum Scopes Needed + +```javascript +// ❌ AVOID: Requesting broad admin access when not needed +scope: "meeting:write:admin user:write:admin recording:write:admin" + +// ✅ PREFER: Request only what you need +scope: "meeting:read user:read" +``` + +### 2. Use Granular Scopes for Specific Operations + +```javascript +// ❌ Classic (broad) +scope: "meeting:write:admin" // Includes create, update, delete, settings, etc. + +// ✅ Granular (specific) +scope: "meeting:write:meeting:admin" // Only create/update meetings +``` + +### 3. Document Required Scopes + +```javascript +/** + * Create a meeting for a user + * Required scope: meeting:write:admin (Classic) or meeting:write:meeting:admin (Granular) + */ +async function createMeeting(userId, meetingData) { + // ... +} +``` + +## Reference Documentation + +- **Classic Scopes** → [../references/classic-scopes.md](../references/classic-scopes.md) +- **Granular Scopes** → [../references/granular-scopes.md](../references/granular-scopes.md) +- **Scope Errors** → [../troubleshooting/scope-issues.md](../troubleshooting/scope-issues.md) diff --git a/partner-built/zoom-plugin/skills/oauth/concepts/state-parameter.md b/partner-built/zoom-plugin/skills/oauth/concepts/state-parameter.md new file mode 100644 index 00000000..fb2a7992 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/concepts/state-parameter.md @@ -0,0 +1,234 @@ +# State Parameter (CSRF Protection) + +The `state` parameter prevents Cross-Site Request Forgery (CSRF) attacks in OAuth flows. + +## What is CSRF in OAuth? + +### Attack Scenario (Without State) + +``` +1. Attacker initiates OAuth flow for victim's account +2. Attacker gets authorization code in callback +3. Attacker tricks victim into visiting callback URL with attacker's code +4. Victim's app exchanges code and links attacker's Zoom account to victim's app account +5. Attacker now has access to victim's app data +``` + +### Protection (With State) + +``` +1. App generates random state before redirecting to OAuth +2. App stores state in user's session +3. Zoom includes state in callback +4. App verifies state matches session +5. If state doesn't match → Reject (CSRF detected) +``` + +## Implementation + +### Step 1: Generate Random State + +```javascript +const crypto = require('crypto'); + +app.get('/auth', (req, res) => { + // Generate cryptographically secure random state + const state = crypto.randomBytes(16).toString('hex'); + + // Store in session (server-side) + req.session.oauthState = state; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('state', state); // Include state + + res.redirect(authURL.toString()); +}); +``` + +### Step 2: Verify State in Callback + +```javascript +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + const sessionState = req.session.oauthState; + + // Verify state matches + if (state !== sessionState) { + return res.status(403).send('Invalid state parameter - possible CSRF attack'); + } + + // Clean up state (one-time use) + delete req.session.oauthState; + + // Proceed with token exchange + const tokens = await exchangeCodeForToken(code); + // ... +}); +``` + +## State Parameter Flow + +``` +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Your App │ │ User Session│ │ Zoom OAuth │ +└──────┬──────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + │ 1. Generate state │ │ + │ state = "abc123" │ │ + │ │ │ + │ 2. Store in session │ │ + │────────────────────────────>│ session.oauthState = "abc123" + │ │ │ + │ 3. Redirect to authorize with state │ + │https://zoom.us/oauth/authorize?state=abc123 │ + │───────────────────────────────────────────────────────────>│ + │ │ │ + │ 4. User authorizes │ │ + │ │ │ + │ 5. Redirect to callback with state │ + │ /callback?code=xyz&state=abc123 │ + │<───────────────────────────────────────────────────────────│ + │ │ │ + │ 6. Retrieve session state │ │ + │<────────────────────────────│ sessionState = "abc123" │ + │ │ │ + │ 7. Verify state === sessionState │ + │ "abc123" === "abc123" ✓ │ │ + │ │ │ + │ 8. Exchange code for token │ │ + │───────────────────────────────────────────────────────────>│ + │ │ │ +``` + +## Common Mistakes + +### 1. Not Verifying State + +```javascript +// ❌ WRONG: Accepting any state +app.get('/callback', async (req, res) => { + const { code } = req.query; + // No state verification! + await exchangeCodeForToken(code); +}); +``` + +```javascript +// ✅ CORRECT: Verifying state +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + if (state !== req.session.oauthState) { + return res.status(403).send('CSRF detected'); + } + + await exchangeCodeForToken(code); +}); +``` + +### 2. Using Predictable State + +```javascript +// ❌ WRONG: Predictable state +const state = Date.now().toString(); // Attacker can predict! +``` + +```javascript +// ✅ CORRECT: Cryptographically random state +const state = crypto.randomBytes(16).toString('hex'); +``` + +### 3. Reusing State + +```javascript +// ❌ WRONG: Not deleting state after use +if (state === req.session.oauthState) { + // State remains in session - can be reused! +} +``` + +```javascript +// ✅ CORRECT: Delete state after verification +if (state === req.session.oauthState) { + delete req.session.oauthState; // One-time use +} +``` + +## State + PKCE Together + +For maximum security, use both: + +```javascript +app.get('/auth', (req, res) => { + // Generate state (CSRF protection) + const state = crypto.randomBytes(16).toString('hex'); + req.session.oauthState = state; + + // Generate PKCE (authorization code interception protection) + const { code_verifier, code_challenge } = generatePKCE(); + req.session.pkceVerifier = code_verifier; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', CLIENT_ID); + authURL.searchParams.set('redirect_uri', REDIRECT_URI); + authURL.searchParams.set('state', state); // CSRF protection + authURL.searchParams.set('code_challenge', code_challenge); // PKCE + authURL.searchParams.set('code_challenge_method', 'S256'); + + res.redirect(authURL.toString()); +}); + +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + // Verify state (CSRF) + if (state !== req.session.oauthState) { + return res.status(403).send('CSRF detected'); + } + + // Exchange with code_verifier (PKCE) + const code_verifier = req.session.pkceVerifier; + const tokens = await exchangeCode(code, code_verifier); + + // Clean up + delete req.session.oauthState; + delete req.session.pkceVerifier; +}); +``` + +## Mobile Apps + +For mobile apps without server-side sessions: + +```javascript +// Store state in secure storage +const state = generateRandomString(); +await SecureStore.setItemAsync('oauth_state', state); + +// Verify in callback +const storedState = await SecureStore.getItemAsync('oauth_state'); +if (receivedState !== storedState) { + throw new Error('CSRF detected'); +} + +// Clean up +await SecureStore.deleteItemAsync('oauth_state'); +``` + +## Best Practices + +1. **Always use state** for User OAuth and Device Flow +2. **Generate cryptographically random state** (not timestamps, sequential IDs) +3. **Store state server-side** in session (not client-side cookies) +4. **Delete state after verification** (one-time use) +5. **Combine with PKCE** for mobile/SPA apps + +## Next Steps + +- **Implement state + PKCE** → [../examples/pkce-implementation.md](../examples/pkce-implementation.md) +- **Understand PKCE** → [pkce.md](pkce.md) +- **Fix OAuth errors** → [../troubleshooting/common-errors.md](../troubleshooting/common-errors.md) diff --git a/partner-built/zoom-plugin/skills/oauth/concepts/token-lifecycle.md b/partner-built/zoom-plugin/skills/oauth/concepts/token-lifecycle.md new file mode 100644 index 00000000..1873dc3f --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/concepts/token-lifecycle.md @@ -0,0 +1,484 @@ +# Token Lifecycle + +Understanding how Zoom OAuth tokens are created, expire, refresh, and revoke is critical for building reliable integrations. + +## Token Types + +### Access Token +- **Purpose:** Authenticate API requests +- **Lifetime:** 1 hour (all OAuth flows) +- **Usage:** `Authorization: Bearer {access_token}` header +- **Format:** Opaque string (not JWT) + +### Refresh Token +- **Purpose:** Obtain new access tokens without user re-authorization +- **Lifetime:** Varies by flow/account/app configuration; ~90 days is common for some user-based flows (treat as changeable behavior) +- **Availability:** S2S OAuth and Chatbot do NOT have refresh tokens +- **Rotation:** Each refresh returns a NEW refresh token (old one becomes invalid) + +### Authorization Code +- **Purpose:** Temporary code exchanged for access token +- **Lifetime:** 5 minutes +- **Usage:** User OAuth and Device Flow only +- **One-time use:** Code becomes invalid after exchange + +--- + +## Expiration Summary + +| Flow | Access Token | Refresh Token | Strategy | +|------|--------------|---------------|----------| +| **S2S OAuth** | 1 hour | None | Request new token before expiration | +| **User OAuth** | 1 hour | ~90 days (commonly) | Use refresh token to get new access token | +| **Device Flow** | 1 hour | ~90 days (commonly) | Use refresh token to get new access token | +| **Chatbot** | 1 hour | None | Request new token before expiration | + +--- + +## S2S OAuth & Chatbot Token Lifecycle + +### Timeline + +``` +┌────────────────────────────────────────────────────┐ +│ │ +│ Token Request │ +│ │ │ +│ v │ +│ [ Access Token Valid ] │ +│ │ +│ ├───────────────────── 1 hour ──────────────────┤ │ +│ │ +│ Token │ +│ Expires │ +│ │ │ +│ v │ +│ Request New Token ────────────────> [ New Access Token Valid ] +│ │ +└────────────────────────────────────────────────────┘ +``` + +### Strategy: Cache with TTL + +```javascript +const redis = require('redis'); +const client = redis.createClient(); + +const getToken = async () => { + // Check cache first + let token = await client.get('zoom_access_token'); + + if (!token) { + // Request new token + const response = await axios.post('https://zoom.us/oauth/token', ...); + const { access_token, expires_in } = response.data; + + // Cache with TTL (10 second buffer before actual expiration) + await client.setex('zoom_access_token', expires_in - 10, access_token); + + token = access_token; + } + + return token; +}; +``` + +**Key Points:** +- ✅ Cache token in Redis with TTL matching expiration +- ✅ Use 10-second buffer to prevent race conditions +- ✅ Single token shared across all requests +- ❌ Do NOT request new token on every API call +- ❌ Do NOT try to "refresh" (no refresh token exists) + +--- + +## User OAuth & Device Flow Token Lifecycle + +### Timeline + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ │ +│ User Authorizes │ +│ │ │ +│ v │ +│ [ Access Token Valid ] │ +│ [ Refresh Token Valid ]────────────────────────────────────────┐ │ +│ │ │ +│ ├───────────── 1 hour ────────────┤ │ │ +│ │ │ +│ Access Token Expires │ │ +│ │ │ │ +│ v │ │ +│ Refresh Request ──────────> [ New Access Token Valid ] │ │ +│ [ New Refresh Token Valid ]────┐ │ │ +│ │ │ │ +│ ├───────────── 1 hour ────────────┤ │ │ │ +│ │ │ │ +│ Access Token Expires │ │ │ +│ │ │ │ │ +│ v │ │ │ +│ Refresh Request ──────────> [ New Access Token Valid ] │ │ │ +│ [ New Refresh Token Valid ] │ │ │ +│ │ │ │ +│ ... Continue refreshing up to ~90 days (commonly) ... │ │ │ +│ │ │ │ +│ ├──────────────────────── ~90 days (commonly) ───────────┤ │ │ +│ │ │ +│ Refresh Token Expires │ │ +│ │ │ │ +│ v │ │ +│ User Must Re-Authorize (restart OAuth flow) │ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +### Strategy: Auto-Refresh Middleware + +```javascript +const tokenMiddleware = async (req, res, next) => { + const userId = req.session.userId; + + // Get user's tokens from database + let { access_token, refresh_token, token_expiry } = await getUserTokens(userId); + + // Check if access token is expired or will expire soon (5 minute buffer) + const now = Date.now(); + const expiresIn = token_expiry - now; + + if (expiresIn < 300000) { // Less than 5 minutes remaining + // Refresh the token + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'refresh_token', + refresh_token: refresh_token + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token: new_access_token, refresh_token: new_refresh_token, expires_in } = response.data; + + // CRITICAL: Update BOTH tokens in database + await updateUserTokens(userId, { + access_token: new_access_token, + refresh_token: new_refresh_token, // NEW refresh token + token_expiry: now + (expires_in * 1000) + }); + + access_token = new_access_token; + } + + // Attach token to request + req.zoomToken = access_token; + next(); +}; +``` + +**Key Points:** +- ✅ Refresh BEFORE token expires (5-minute buffer recommended) +- ✅ **ALWAYS save the NEW refresh token** (old one becomes invalid) +- ✅ Store tokens per user in database +- ✅ Encrypt tokens at rest (AES-256 minimum) +- ❌ Do NOT reuse old refresh token after refresh +- ❌ Do NOT wait for API 401 errors to trigger refresh + +--- + +## Refresh Token Rotation + +**CRITICAL:** Zoom rotates refresh tokens on every refresh. + +### What Happens During Refresh + +``` +Before Refresh: + access_token: "abc123" (expired) + refresh_token: "xyz789" (valid) + +Request: + POST /oauth/token + grant_type=refresh_token + refresh_token=xyz789 + +Response: + { + "access_token": "def456", // NEW access token + "refresh_token": "uvw012", // NEW refresh token + "expires_in": 3600 + } + +After Refresh: + access_token: "def456" (valid for 1 hour) + refresh_token: "uvw012" (lifetime varies; ~90 days is common) + + OLD refresh_token "xyz789" is NOW INVALID +``` + +### Common Mistake + +```javascript +// ❌ WRONG: Not saving new refresh token +const response = await refreshToken(old_refresh_token); +const { access_token } = response.data; // Only saving access token + +await updateUserTokens(userId, { access_token }); // Refresh token not updated! + +// Next refresh will fail with error 4735 "Invalid refresh token" +``` + +```javascript +// ✅ CORRECT: Saving both tokens +const response = await refreshToken(old_refresh_token); +const { access_token, refresh_token } = response.data; + +await updateUserTokens(userId, { + access_token, + refresh_token // MUST save new refresh token +}); +``` + +--- + +## Authorization Code Expiration + +**Lifetime:** 5 minutes + +### Timeline + +``` +User Clicks "Allow" + │ + v +Authorization Code Issued (expires in 5 minutes) + │ + │ ← Exchange code for token within 5 minutes + v +[ Access Token + Refresh Token ] + +If code not exchanged within 5 minutes: + → Error 4733 "Invalid authorization code" + → User must re-authorize +``` + +### Implementation + +```javascript +app.get('/callback', async (req, res) => { + const { code } = req.query; + + try { + // Exchange code for token IMMEDIATELY + const response = await axios.post('https://zoom.us/oauth/token', { + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.REDIRECT_URI + }, ...); + + // Store tokens + await saveTokens(response.data); + + } catch (error) { + if (error.response?.data?.error === 'invalid_grant') { + // Code expired (4733) or already used + res.send('Authorization code expired. Please re-authorize.'); + } + } +}); +``` + +**Key Points:** +- ✅ Exchange authorization code **immediately** upon receiving it +- ✅ Authorization codes are one-time use +- ❌ Do NOT cache or store authorization codes +- ❌ Do NOT delay token exchange + +--- + +## Token Revocation + +### When Tokens Are Revoked + +1. **User re-authorizes your app:** + - All previous tokens for that user become invalid + - New tokens are issued + +2. **User removes your app:** + - All tokens for that user become invalid + - User must re-authorize to grant access again + +3. **Explicit revocation:** + - Your app calls `https://zoom.us/oauth/revoke` endpoint + - Tokens become invalid immediately + +4. **Refresh token expires (lifetime varies):** + - Can no longer refresh + - User must re-authorize + +### Revoke Token API + +```javascript +const revokeToken = async (access_token) => { + await axios.post( + 'https://zoom.us/oauth/revoke', + qs.stringify({ + token: access_token + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + // Token is now revoked + // Delete from database + await deleteUserTokens(userId); +}; +``` + +**What Gets Revoked:** +- Access token becomes invalid immediately +- Refresh token becomes invalid immediately +- All API requests with revoked token return 401 + +--- + +## Error Codes + +| Code | Error | Meaning | Action | +|------|-------|---------|--------| +| **4733** | Invalid authorization code | Code expired (5 min) or already used | User must re-authorize | +| **4735** | Invalid refresh token | Refresh token expired or rotated | User must re-authorize | +| **4741** | Token has been revoked | Token was explicitly revoked | User must re-authorize | +| **401** | Unauthorized | Access token expired or invalid | Refresh token (if available) or re-authorize | + +--- + +## Best Practices + +### 1. Cache S2S Tokens + +```javascript +// ✅ Cache in Redis with TTL +await redis.setex('zoom_token', expires_in - 10, access_token); +``` + +```javascript +// ❌ Request new token on every API call +const token = await getToken(); // Every time? No! +await makeAPIRequest(token); +``` + +### 2. Refresh BEFORE Expiration + +```javascript +// ✅ Refresh with buffer (5 minutes before expiry) +if (expiresIn < 300000) { + await refreshToken(); +} +``` + +```javascript +// ❌ Wait for 401 error +try { + await makeAPIRequest(token); +} catch (err) { + if (err.status === 401) { + await refreshToken(); // Too late! + } +} +``` + +### 3. Always Save New Refresh Token + +```javascript +// ✅ Update both tokens +const { access_token, refresh_token } = await refresh(); +await saveTokens({ access_token, refresh_token }); +``` + +```javascript +// ❌ Only save access token +const { access_token } = await refresh(); +await saveTokens({ access_token }); // Refresh token not saved! +``` + +### 4. Encrypt Tokens at Rest + +```javascript +// ✅ Encrypt before storing +const encrypted = encrypt(access_token, CIPHER_KEY); +await db.query('UPDATE users SET token = ? WHERE id = ?', [encrypted, userId]); +``` + +```javascript +// ❌ Store in plain text +await db.query('UPDATE users SET token = ? WHERE id = ?', [access_token, userId]); +``` + +### 5. Handle Revocation Gracefully + +```javascript +// ✅ Detect revoked tokens and prompt re-auth +if (error.code === 4741) { + await deleteUserTokens(userId); + res.redirect('/auth'); // Re-authorize +} +``` + +--- + +## Debugging Token Issues + +### Symptom: "Token expired" immediately after getting it + +**Cause:** Server clock is incorrect + +**Solution:** +```bash +# Sync server time +sudo ntpdate -s time.nist.gov +``` + +### Symptom: Refresh fails with "Invalid refresh token" (4735) + +**Cause:** Using old refresh token after it was rotated + +**Solution:** +- Check database: Are you saving the NEW refresh token? +- Check code: Are you updating BOTH access_token AND refresh_token? + +### Symptom: Authorization code fails with "Invalid grant" (4733) + +**Cause:** Code expired (5 minutes passed) or already used + +**Solution:** +- Exchange code immediately in callback +- Codes are one-time use (don't cache) + +### Symptom: All tokens revoked unexpectedly + +**Cause:** User re-authorized your app or removed it + +**Solution:** +- Detect 401/4741 errors +- Prompt user to re-authorize + +--- + +## Next Steps + +- **Implement auto-refresh** → [../examples/token-refresh.md](../examples/token-refresh.md) +- **Fix token errors** → [../troubleshooting/token-issues.md](../troubleshooting/token-issues.md) +- **Understand OAuth flows** → [oauth-flows.md](oauth-flows.md) diff --git a/partner-built/zoom-plugin/skills/oauth/examples/device-flow.md b/partner-built/zoom-plugin/skills/oauth/examples/device-flow.md new file mode 100644 index 00000000..89c450bb --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/device-flow.md @@ -0,0 +1,9 @@ +# Device Flow + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/examples/pkce-implementation.md b/partner-built/zoom-plugin/skills/oauth/examples/pkce-implementation.md new file mode 100644 index 00000000..5388259b --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/pkce-implementation.md @@ -0,0 +1,9 @@ +# Pkce Implementation + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-basic.md b/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-basic.md new file mode 100644 index 00000000..dee7cee9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-basic.md @@ -0,0 +1,9 @@ +# S2s Oauth Basic + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-redis.md b/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-redis.md new file mode 100644 index 00000000..69822341 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/s2s-oauth-redis.md @@ -0,0 +1,295 @@ +# S2S OAuth with Redis Caching (Production Pattern) + +Production-ready Server-to-Server OAuth implementation with Redis token caching and auto-refresh middleware. + +## Architecture + +``` +Express App + ↓ +tokenCheck Middleware (automatic token management) + ↓ +Redis Cache (TTL-based expiration) + ↓ +Zoom API Routes (protected) +``` + +## Complete Implementation + +### 1. Dependencies + +```bash +npm install express redis axios query-string dotenv +``` + +### 2. Redis Configuration + +```javascript +// configs/redis.js +const redis = require('redis'); + +const client = redis.createClient({ + url: process.env.REDIS_URL || 'redis://YOUR_REDIS_HOST:6379' +}); + +client.on('error', (err) => console.error('Redis error:', err)); +client.on('connect', () => console.log('Connected to Redis')); + +module.exports = client; +``` + +### 3. Token Utilities + +```javascript +// utils/token.js +const axios = require('axios'); +const qs = require('query-string'); + +const { ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET } = process.env; + +const getToken = async () => { + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'account_credentials', + account_id: ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope } + } catch (error) { + throw new Error(`Token request failed: ${error.response?.data?.message || error.message}`); + } +}; + +const setToken = async (redis, { access_token, expires_in }) => { + // Cache with TTL (10 second buffer before actual expiration) + await redis.setex('access_token', expires_in - 10, access_token); +}; + +module.exports = { getToken, setToken }; +``` + +### 4. Token Check Middleware + +```javascript +// middlewares/tokenCheck.js +const redis = require('../configs/redis'); +const { getToken, setToken } = require('../utils/token'); + +const tokenCheck = async (req, res, next) => { + let token = await redis.get('access_token'); + + // Redis returns null if key doesn't exist + if (!token) { + try { + const { access_token, expires_in, error } = await getToken(); + + if (error) { + return res.status(401).json({ + message: `Authentication failed: ${error.message}` + }); + } + + // Cache token + await setToken(redis, { access_token, expires_in }); + token = access_token; + } catch (err) { + return res.status(500).json({ + message: 'Token generation failed', + error: err.message + }); + } + } + + // Attach token to request for route handlers + req.headerConfig = { + headers: { + Authorization: `Bearer ${token}` + } + }; + + next(); +}; + +module.exports = { tokenCheck }; +``` + +### 5. Main Application + +```javascript +// index.js +require('dotenv').config(); + +const express = require('express'); +const redis = require('./configs/redis'); +const { tokenCheck } = require('./middlewares/tokenCheck'); + +const app = express(); +const PORT = process.env.PORT || 8080; + +// Connect to Redis +(async () => { + await redis.connect(); +})(); + +// Add global middlewares +app.use(express.json()); + +// Apply tokenCheck to all API routes +app.use('/api/users', tokenCheck, require('./routes/api/users')); +app.use('/api/meetings', tokenCheck, require('./routes/api/meetings')); + +const server = app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); +}); + +// Graceful shutdown +const cleanup = async () => { + console.log('Shutting down gracefully...'); + await redis.del('access_token'); // Clear cached token + server.close(() => { + redis.quit(() => process.exit()); + }); +}; + +process.on('SIGTERM', cleanup); +process.on('SIGINT', cleanup); +``` + +### 6. Example API Route + +```javascript +// routes/api/users.js +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const ZOOM_API_BASE = 'https://api.zoom.us/v2'; + +// List users +router.get('/', async (req, res) => { + try { + const response = await axios.get( + `${ZOOM_API_BASE}/users`, + req.headerConfig // Token from middleware + ); + res.json(response.data); + } catch (error) { + res.status(error.response?.status || 500).json({ + message: 'Failed to list users', + error: error.response?.data || error.message + }); + } +}); + +// Get user +router.get('/:userId', async (req, res) => { + try { + const response = await axios.get( + `${ZOOM_API_BASE}/users/${req.params.userId}`, + req.headerConfig + ); + res.json(response.data); + } catch (error) { + res.status(error.response?.status || 500).json({ + message: 'Failed to get user', + error: error.response?.data || error.message + }); + } +}); + +module.exports = router; +``` + +### 7. Environment Variables + +```bash +# .env +ZOOM_ACCOUNT_ID=your_account_id +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret +REDIS_URL=redis://YOUR_REDIS_HOST:6379 +PORT=8080 +``` + +## How It Works + +1. **Request arrives** at protected route (e.g., GET /api/users) +2. **tokenCheck middleware** runs: + - Checks Redis for cached token + - If missing: Requests new token from Zoom + - Caches token with TTL (expires_in - 10 seconds) +3. **Token attached** to `req.headerConfig` +4. **Route handler** makes API request with token +5. **Token auto-refreshes** when Redis TTL expires + +## Benefits + +✅ **Automatic token management** - No manual refresh logic +✅ **Single token for all requests** - Account-wide access +✅ **TTL-based expiration** - Redis handles cleanup +✅ **10-second buffer** - Prevents race conditions +✅ **Graceful shutdown** - Clears token on exit + +## Testing + +```bash +# Start Redis +docker run -d -p 6379:6379 redis + +# Start app +npm start + +# Test endpoints +API_BASE_URL="http://YOUR_API_HOST:8080" +curl "$API_BASE_URL/api/users" +``` + +## Docker Deployment + +```dockerfile +# Dockerfile +FROM node:18 +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "index.js"] +``` + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" + + app: + build: . + ports: + - "8080:8080" + environment: + - ZOOM_ACCOUNT_ID=${ZOOM_ACCOUNT_ID} + - ZOOM_CLIENT_ID=${ZOOM_CLIENT_ID} + - ZOOM_CLIENT_SECRET=${ZOOM_CLIENT_SECRET} + - REDIS_URL=redis://redis:6379 + depends_on: + - redis +``` + +## Related + +- **S2S OAuth flow** → [../concepts/oauth-flows.md#server-to-server-s2s-oauth](../concepts/oauth-flows.md#server-to-server-s2s-oauth) +- **Token lifecycle** → [../concepts/token-lifecycle.md](../concepts/token-lifecycle.md) diff --git a/partner-built/zoom-plugin/skills/oauth/examples/token-refresh.md b/partner-built/zoom-plugin/skills/oauth/examples/token-refresh.md new file mode 100644 index 00000000..96079d89 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/token-refresh.md @@ -0,0 +1,9 @@ +# Token Refresh + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-basic.md b/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-basic.md new file mode 100644 index 00000000..685bb38e --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-basic.md @@ -0,0 +1,9 @@ +# User Oauth Basic + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-mysql.md b/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-mysql.md new file mode 100644 index 00000000..72eaae12 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/examples/user-oauth-mysql.md @@ -0,0 +1,9 @@ +# User Oauth Mysql + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/partner-built/zoom-plugin/skills/oauth/references/classic-scopes.md b/partner-built/zoom-plugin/skills/oauth/references/classic-scopes.md new file mode 100644 index 00000000..3d401f1b --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/references/classic-scopes.md @@ -0,0 +1,6270 @@ +# Classic OAuth Scopes + +> Source: https://developers.zoom.us/docs/integrations/oauth-scopes/ + +## Account + +### account:master + +View and manage sub accounts + +**Associated APIs:** + +- [Update the account owner](/docs/api/rest/reference/account/ma/#operation/UpdateTheAccountOwner) +- [Get sub account details](/docs/api/rest/reference/account/ma/#operation/GetSubAccountDetails) +- [Get account's managed domains](/docs/api/rest/reference/account/ma/#operation/accountManagedDomain) +- [List upcoming renewal accounts](/docs/api/rest/reference/billing/ma/#operation/Getupcomingrenewalaccounts) +- [Delete virtual background files](/docs/api/rest/reference/account/ma/#operation/delVB) +- [Create a sub account](/docs/api/rest/reference/account/ma/#operation/accountCreate) +- [Update options](/docs/api/rest/reference/account/ma/#operation/UpdateOptions) +- [Get plan usage](/docs/api/rest/reference/billing/ma/#operation/GetPlanUsage) +- [Get account's trusted domains](/docs/api/rest/reference/account/ma/#operation/accountTrustedDomain) +- [List sub accounts](/docs/api/rest/reference/account/ma/#operation/accounts) +- [Disassociate a sub account](/docs/api/rest/reference/account/ma/#operation/DisassociateASubAccount) +- [Upload virtual background files](/docs/api/rest/reference/account/ma/#operation/uploadVB) + +### account:read:admin + +View account info + +**Associated APIs:** + +- [Get locked settings](/docs/api/rest/reference/account/methods/#operation/getAccountLockSettings) +- [Get indicators settings](/docs/api/rest/reference/iq/methods/#operation/accountIndicatorsSettings) +- [Get an account's webinar registration settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsRegistration) +- [Get account settings](/docs/api/rest/reference/account/ma/#operation/accountSettings) +- [Get account's managed domains](/docs/api/rest/reference/account/methods/#operation/accountManagedDomain) +- [Get account's trusted domains](/docs/api/rest/reference/account/methods/#operation/accountTrustedDomain) +- [Get indicators settings [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/accountSettingsIndicatorsDeprecated) + +### account:write:admin + +View and manage account info + +**Associated APIs:** + +- [Update locked settings](/docs/api/rest/reference/account/methods/#operation/UpdateLockedSettings) +- [Get locked settings](/docs/api/rest/reference/account/ma/#operation/getAccountLockSettings) +- [Get indicators settings](/docs/api/rest/reference/iq/methods/#operation/accountIndicatorsSettings) +- [Update the account owner](/docs/api/rest/reference/account/methods/#operation/UpdateTheAccountOwner) +- [Upload virtual background files](/docs/api/rest/reference/account/ma/#operation/uploadVB) +- [Update account settings](/docs/api/rest/reference/account/methods/#operation/accountSettingsUpdate) +- [Get indicators settings [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/accountSettingsIndicatorsDeprecated) +- [Get an account's webinar registration settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsRegistration) +- [Delete virtual background files](/docs/api/rest/reference/account/ma/#operation/delVB) +- [Update an account's webinar registration settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsRegistrationUpdate) +- [Get account settings](/docs/api/rest/reference/account/ma/#operation/accountSettings) + +## Billing + +### billing:master + +View and manage sub account's billing info + +**Associated APIs:** + +- [Subscribe an account to a plan](/docs/api/rest/reference/billing/ma/#operation/SubscribeAccountToAPlan) +- [Get invoice details](/docs/api/rest/reference/billing/ma/#operation/GetInvoiceDetails) +- [Cancel additional plans](/docs/api/rest/reference/billing/ma/#operation/CancelAdditionalPlans) +- [Get account plan information](/docs/api/rest/reference/billing/ma/#operation/GetAccountPlanInformation) +- [Generate sub accounts' billing invoice reports](/docs/api/rest/reference/billing/ma/#operation/generateSubInvoiceReport) +- [Get billing information](/docs/api/rest/reference/billing/ma/#operation/GetBillingInformation) +- [Delete subaccounts' billing invoice report](/docs/api/rest/reference/billing/ma/#operation/deleteBillingInvoiceReport) +- [Subscribe subaccount to an additional plan](/docs/api/rest/reference/billing/ma/#operation/SubscribeAccountToAnAdditionalPlan) +- [Update an account's additional plan](/docs/api/rest/reference/billing/ma/#operation/UpdateAnAccount'sAdditionalPlan) +- [Download an invoice file](/docs/api/rest/reference/billing/ma/#operation/downloadInvoicePDF) +- [Update a base plan](/docs/api/rest/reference/billing/ma/#operation/UpdateABasePlan) +- [List upcoming renewal accounts](/docs/api/rest/reference/billing/ma/#operation/Getupcomingrenewalaccounts) +- [List billing invoices](/docs/api/rest/reference/billing/ma/#operation/ListBillingInvoices) +- [List sub accounts' billing invoice reports](/docs/api/rest/reference/billing/ma/#operation/listSubInvoiceReport) +- [Download subaccounts' billing invoice reports](/docs/api/rest/reference/billing/ma/#operation/downloadBillingInvoiceReport) +- [Download an invoice file (v2)](/docs/api/rest/reference/billing/ma/#operation/downloadInvoicePDFFile) +- [Cancel a base plan](/docs/api/rest/reference/billing/ma/#operation/CancelABasePlan) +- [Update billing information](/docs/api/rest/reference/billing/ma/#operation/UpdateBillingInformation) +- [Get plan usage](/docs/api/rest/reference/billing/ma/#operation/GetPlanUsage) + +## Calendar + +### calendar:read + +View calendar + +**Associated APIs:** + +- [Get the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getsetting) +- [Get the color definitions for calendars and events](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcolor) +- [Query freebusy information for a set of calendars](/docs/api/rest/reference/zoom-calendar/methods/#operation/Queryfreebusy) +- [List ACL rules of specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listacl) +- [List all user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listsettings) +- [Get a specified calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/GetcalendarList) +- [Get the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getevent) +- [List events on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listevent) +- [List the calendars in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/ListcalendarList) +- [Get the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcalendar) +- [List all instances of the specified recurring event](/docs/api/rest/reference/zoom-calendar/methods/#operation/Instanceevent) +- [Get the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getacl) + +### calendar:read:admin + +View calendar + +**Associated APIs:** + +- [Get the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcalendar) +- [Get the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getevent) +- [Query freebusy information for a set of calendars](/docs/api/rest/reference/zoom-calendar/methods/#operation/Queryfreebusy) +- [List ACL rules of specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listacl) +- [Get the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getsetting) +- [List all user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listsettings) +- [List all instances of the specified recurring event](/docs/api/rest/reference/zoom-calendar/methods/#operation/Instanceevent) +- [Get the color definitions for calendars and events](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcolor) +- [Get the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getacl) +- [List events on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listevent) +- [Get a specified calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/GetcalendarList) +- [List the calendars in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/ListcalendarList) + +### calendar:write + +Manage calendar + +**Associated APIs:** + +- [Insert an existing calendar to the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/InsertcalendarList) +- [Insert a new event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertevent) +- [Quick add an event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Quickaddevent) +- [Delete a calendar owned by a user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deletecalendar) +- [Move the specified event from a calendar to another specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Moveevent) +- [Update the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchcalendar) +- [Import event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Importevent) +- [Create a new secondary calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertcalendar) +- [Create a new ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertacl) +- [Delete an existing ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteacl) +- [Delete an existing calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/DeletecalendarList) +- [Update the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchevent) +- [Update the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchacl) +- [Update an existing calendar in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/PatchcalendarList) +- [Patch the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchsetting) +- [Delete an existing event from the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteevent) + +### calendar:write:admin + +Manage calendar + +**Associated APIs:** + +- [Delete an existing ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteacl) +- [Create a new ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertacl) +- [Delete an existing event from the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteevent) +- [Quick add an event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Quickaddevent) +- [Delete an existing calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/DeletecalendarList) +- [Update the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchevent) +- [Insert an existing calendar to the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/InsertcalendarList) +- [Insert a new event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertevent) +- [Create a new secondary calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertcalendar) +- [Update an existing calendar in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/PatchcalendarList) +- [Patch the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchsetting) +- [Import event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Importevent) +- [Update the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchacl) +- [Update the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchcalendar) +- [Delete a calendar owned by a user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deletecalendar) +- [Move the specified event from a calendar to another specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Moveevent) + +## Chat + +### chat:read + +View Chat Message Info + +**Associated APIs:** + +- [List shared space members](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceMembers) +- [List shared spaces](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaces) +- [List custom emojis](/docs/api/rest/reference/chat/methods/#operation/listCustomEmojis) +- [Get file info](/docs/api/rest/reference/chat/methods/#operation/getFileInfo) +- [List pinned history messages of channel](/docs/api/rest/reference/chat/methods/#operation/listChannelPinnedMessages) +- [Get a shared space](/docs/api/rest/reference/chat/methods/#operation/getASharedSpace) +- [List shared space channels](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceChannels) + +### chat:read:admin + +View Chat Message Info + +**Associated APIs:** + +- [List custom emojis](/docs/api/rest/reference/chat/methods/#operation/listCustomEmojis) +- [List shared space channels](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceChannels) +- [Get a shared space](/docs/api/rest/reference/chat/methods/#operation/getASharedSpace) +- [List shared space members](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceMembers) +- [List pinned history messages of channel](/docs/api/rest/reference/chat/methods/#operation/listChannelPinnedMessages) +- [List shared spaces](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaces) + +### chat:write + +Manage Chat Message Info + +**Associated APIs:** + +- [Delete a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpace) +- [Create a shared space](/docs/api/rest/reference/chat/methods/#operation/createSpace) +- [List shared space members](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceMembers) +- [Add a custom emoji](/docs/api/rest/reference/chat/methods/#operation/addACustomEmoji) +- [Demote shared space administrators to members](/docs/api/rest/reference/chat/methods/#operation/demoteSpaceAdmins) +- [List custom emojis](/docs/api/rest/reference/chat/methods/#operation/listCustomEmojis) +- [Promote shared space members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteSpaceMembers) +- [Remove members from a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpaceMembers) +- [Delete a chat file](/docs/api/rest/reference/chat/methods/#operation/deleteChatFile) +- [Update shared space settings](/docs/api/rest/reference/chat/methods/#operation/updateSharedSpaceSettings) +- [Transfer shared space ownership](/docs/api/rest/reference/chat/methods/#operation/transferSpaceOwner) +- [Move shared space channels](/docs/api/rest/reference/chat/methods/#operation/updateSharedSpaceChannels) +- [List pinned history messages of channel](/docs/api/rest/reference/chat/methods/#operation/listChannelPinnedMessages) +- [Delete a custom emoji](/docs/api/rest/reference/chat/methods/#operation/DeleteCustomEmoji) +- [Get a shared space](/docs/api/rest/reference/chat/methods/#operation/getASharedSpace) +- [List shared space channels](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceChannels) +- [Perform operations on the message of channel](/docs/api/rest/reference/chat/methods/#operation/PerformMessageOfChannel) +- [Add members to a shared space](/docs/api/rest/reference/chat/methods/#operation/addSpaceMembers) +- [List shared spaces](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaces) + +### chat:write:admin + +Manage Chat Message Info + +**Associated APIs:** + +- [List custom emojis](/docs/api/rest/reference/chat/methods/#operation/listCustomEmojis) +- [Create a shared space](/docs/api/rest/reference/chat/methods/#operation/createSpace) +- [Demote shared space administrators to members](/docs/api/rest/reference/chat/methods/#operation/demoteSpaceAdmins) +- [Add a custom emoji](/docs/api/rest/reference/chat/methods/#operation/addACustomEmoji) +- [List shared spaces](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaces) +- [Promote shared space members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteSpaceMembers) +- [Update shared space settings](/docs/api/rest/reference/chat/methods/#operation/updateSharedSpaceSettings) +- [Remove members from a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpaceMembers) +- [Delete a custom emoji](/docs/api/rest/reference/chat/methods/#operation/DeleteCustomEmoji) +- [Add members to a shared space](/docs/api/rest/reference/chat/methods/#operation/addSpaceMembers) +- [Get a shared space](/docs/api/rest/reference/chat/methods/#operation/getASharedSpace) +- [List shared space channels](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceChannels) +- [Delete a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpace) +- [List shared space members](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceMembers) +- [Transfer shared space ownership](/docs/api/rest/reference/chat/methods/#operation/transferSpaceOwner) + +### chat_channel:read + +View current user's team chat channels + +**Associated APIs:** + +- [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getUserLevelChannel) +- [List channel members](/docs/api/rest/reference/chat/methods/#operation/listChannelMembers) +- [List channel administrators](/docs/api/rest/reference/chat/methods/#operation/listChannelAdministrators) +- [List the members of a mention group](/docs/api/rest/reference/chat/methods/#operation/listTheMembersOfMentionGroup) +- [Search user's or account's channels](/docs/api/rest/reference/chat/methods/#operation/searchChannels) +- [List channel mention groups](/docs/api/rest/reference/chat/methods/#operation/getChannelMentionGroup) +- [List user's channels](/docs/api/rest/reference/chat/methods/#operation/getChannels) + +### chat_channel:read:admin + +View all users' team chat channels + +**Associated APIs:** + +- [List channel activity logs](/docs/api/rest/reference/chat/methods/#operation/listChannelActivityLogs) +- [List user's channels](/docs/api/rest/reference/chat/methods/#operation/getChannels) +- [List channel members (Groups)](/docs/api/rest/reference/chat/methods/#operation/listChannelMembersGroups) +- [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getUserLevelChannel) +- [List channel mention groups](/docs/api/rest/reference/chat/methods/#operation/getChannelMentionGroup) +- [List channel members](/docs/api/rest/reference/chat/methods/#operation/listChannelMembers) +- [List channel administrators](/docs/api/rest/reference/chat/methods/#operation/listChannelAdministrators) +- [List account's public channels](/docs/api/rest/reference/chat/methods/#operation/getAccountChannels) +- [Search user's or account's channels](/docs/api/rest/reference/chat/methods/#operation/searchChannels) +- [Get retention policy of a channel](/docs/api/rest/reference/chat/methods/#operation/getChannelRetention) + +### chat_channel:read:member + +View team chat channels as a member + +**Associated APIs:** + +- [List the members of a mention group](/docs/api/rest/reference/chat/methods/#operation/listTheMembersOfMentionGroup) + +### chat_channel:write + +View and manage current user's team chat channels + +**Associated APIs:** + +- [List user's channels](/docs/api/rest/reference/chat/methods/#operation/getChannels) +- [Batch demote channel administrators](/docs/api/rest/reference/chat/methods/#operation/batchDemoteChannelAdministrators) +- [Leave a channel](/docs/api/rest/reference/chat/methods/#operation/leaveChannel) +- [Perform operations on channels](/docs/api/rest/reference/chat/methods/#operation/PerformOperationsOnChannels) +- [Join a channel](/docs/api/rest/reference/chat/methods/#operation/joinChannel) +- [Search user's or account's channels](/docs/api/rest/reference/chat/methods/#operation/searchChannels) +- [Add channel members to a mention group](/docs/api/rest/reference/chat/methods/#operation/addAChannelMembersToMentionGroup) +- [Update a channel](/docs/api/rest/reference/chat/methods/#operation/updateUserLevelChannel) +- [Delete a channel mention group](/docs/api/rest/reference/chat/methods/#operation/deleteAChannelMentionGroup) +- [List channel members](/docs/api/rest/reference/chat/methods/#operation/listChannelMembers) +- [Invite channel members](/docs/api/rest/reference/chat/methods/#operation/InviteUserLevelChannelMembers) +- [Batch delete channels](/docs/api/rest/reference/chat/methods/#operation/batchDeleteChannelsAccountLevel) +- [Update a channel mention group information](/docs/api/rest/reference/chat/methods/#operation/updateChannelMentionGroup) +- [Create a channel mention group](/docs/api/rest/reference/chat/methods/#operation/createChannelMentionGroup) +- [Promote channel members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteChannelMembersAsAdmin) +- [Batch remove members from a user's channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveUserChannelMembers) +- [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getUserLevelChannel) +- [Delete a channel](/docs/api/rest/reference/chat/methods/#operation/deleteChannel) +- [Batch remove members from a channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveChannelMembers) +- [Remove a member](/docs/api/rest/reference/chat/methods/#operation/removeAUserLevelChannelMember) +- [Create a channel](/docs/api/rest/reference/chat/methods/#operation/createChannel) +- [Remove channel mention group members](/docs/api/rest/reference/chat/methods/#operation/removeChannelMentionGroupMembers) + +### chat_channel:write:admin + +View and manage all users' team chat channels + +**Associated APIs:** + +- [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getChannel) +- [Update a channel](/docs/api/rest/reference/chat/methods/#operation/updateChannel) +- [Batch remove members from a channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveChannelMembers) +- [Delete a channel](/docs/api/rest/reference/chat/methods/#operation/deleteChannel) +- [List channel members](/docs/api/rest/reference/chat/methods/#operation/listUserLevelChannelMembers) +- [Get retention policy of a channel](/docs/api/rest/reference/chat/methods/#operation/getChannelRetention) +- [Remove a member](/docs/api/rest/reference/chat/methods/#operation/removeAChannelMember) +- [List the members of a mention group](/docs/api/rest/reference/chat/methods/#operation/listTheMembersOfMentionGroup) +- [Update retention policy of a channel](/docs/api/rest/reference/chat/methods/#operation/updateChannelRetention) +- [Create a channel](/docs/api/rest/reference/chat/methods/#operation/createChannel) +- [Join a channel](/docs/api/rest/reference/chat/methods/#operation/joinChannel) +- [Remove channel mention group members](/docs/api/rest/reference/chat/methods/#operation/removeChannelMentionGroupMembers) +- [Search user's or account's channels](/docs/api/rest/reference/chat/methods/#operation/searchChannels) +- [Invite channel members (Groups)](/docs/api/rest/reference/chat/methods/#operation/inviteChannelMembersGroups) +- [Invite channel members](/docs/api/rest/reference/chat/methods/#operation/InviteUserLevelChannelMembers) +- [Batch demote channel administrators](/docs/api/rest/reference/chat/methods/#operation/batchDemoteChannelAdministrators) +- [List channel members (Groups)](/docs/api/rest/reference/chat/methods/#operation/listChannelMembersGroups) +- [Leave a channel](/docs/api/rest/reference/chat/methods/#operation/leaveChannel) +- [Remove a member (group)](/docs/api/rest/reference/chat/methods/#operation/removeAMemberGroup) +- [Perform operations on channels](/docs/api/rest/reference/chat/methods/#operation/PerformOperationsOnChannels) +- [Add channel members to a mention group](/docs/api/rest/reference/chat/methods/#operation/addAChannelMembersToMentionGroup) +- [List user's channels](/docs/api/rest/reference/chat/methods/#operation/getChannels) +- [Delete a channel mention group](/docs/api/rest/reference/chat/methods/#operation/deleteAChannelMentionGroup) +- [List account's public channels](/docs/api/rest/reference/chat/methods/#operation/getAccountChannels) +- [Batch delete channels](/docs/api/rest/reference/chat/methods/#operation/batchDeleteChannelsAccountLevel) +- [Update a channel mention group information](/docs/api/rest/reference/chat/methods/#operation/updateChannelMentionGroup) +- [Promote channel members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteChannelMembersAsAdmin) +- [Create a channel mention group](/docs/api/rest/reference/chat/methods/#operation/createChannelMentionGroup) +- [Batch remove members from a user's channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveUserChannelMembers) + +### chat_contact:read + +View current user's team chat contact information + +**Associated APIs:** + +- [Get user's contact details](/docs/api/rest/reference/chat/methods/#operation/getUserContact) +- [List user's contacts](/docs/api/rest/reference/chat/methods/#operation/getUserContacts) + +### chat_contact:write + +Send Contact Invitation + +**Associated APIs:** + +- [Send new contact invitation](/docs/api/rest/reference/chat/methods/#operation/sendNewContactInvitation) + +### chat_contact:write:admin + +Send contact invitation + +**Associated APIs:** + +- [Send new contact invitation](/docs/api/rest/reference/chat/methods/#operation/sendNewContactInvitation) + +### chat_event:write + +Manage your chat event info + +**Associated APIs:** + +- [Star or unstar a channel or contact user](/docs/api/rest/reference/chat/methods/#operation/starUnstarChannelContact) + +### chat_event:write:admin + +Manage account's chat event info + +**Associated APIs:** + +- [Star or unstar a channel or contact user](/docs/api/rest/reference/chat/methods/#operation/starUnstarChannelContact) + +### chat_history_legal_hold:read:admin + +View Chat History Legal Hold Info + +**Associated APIs:** + +- [List legal hold files by given matter](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldFiles) +- [Download legal hold files for given matter](/docs/api/rest/reference/chat/methods/#operation/downloadLegalHoldFiles) +- [List legal hold matters](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldMatters) + +### chat_history_legal_hold:write:admin + +Manage Chat History Legal Hold Info + +**Associated APIs:** + +- [Update legal hold matter](/docs/api/rest/reference/chat/methods/#operation/updateLegalHoldMatter) +- [List legal hold files by given matter](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldFiles) +- [Download legal hold files for given matter](/docs/api/rest/reference/chat/methods/#operation/downloadLegalHoldFiles) +- [List legal hold matters](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldMatters) +- [Delete legal hold matters](/docs/api/rest/reference/chat/methods/#operation/deleteLegalHoldMatters) +- [Add a legal hold matter](/docs/api/rest/reference/chat/methods/#operation/addLegalHoldMatter) + +### chat_message:read + +View current user's team chat messages + +**Associated APIs:** + +- [Get a forwarded message](/docs/api/rest/reference/chat/methods/#operation/getForwardedMessage) +- [Retrieve a thread](/docs/api/rest/reference/chat/methods/#operation/retrieveThread) +- [Get a message](/docs/api/rest/reference/chat/methods/#operation/getChatMessage) +- [List reminders](/docs/api/rest/reference/chat/methods/#operation/listReminders) +- [List bookmarks](/docs/api/rest/reference/chat/methods/#operation/fetchBookmarks) +- [List a user's chat sessions](/docs/api/rest/reference/chat/methods/#operation/getChatSessions) +- [List scheduled messages](/docs/api/rest/reference/chat/methods/#operation/listScheduledMessages) +- [List user's chat messages](/docs/api/rest/reference/chat/methods/#operation/getChatMessages) + +### chat_message:read:admin + +View all users' team chat messages + +**Associated APIs:** + +- [Get a message](/docs/api/rest/reference/chat/methods/#operation/getChatMessage) +- [Retrieve a thread](/docs/api/rest/reference/chat/methods/#operation/retrieveThread) +- [List user's chat messages](/docs/api/rest/reference/chat/methods/#operation/getChatMessages) +- [Get a forwarded message](/docs/api/rest/reference/chat/methods/#operation/getForwardedMessage) +- [List a user's chat sessions](/docs/api/rest/reference/chat/methods/#operation/getChatSessions) + +### chat_message:write + +View and manage current user's team chat messages + +**Associated APIs:** + +- [Mark message read or unread](/docs/api/rest/reference/chat/methods/#operation/markMessage) +- [Create a reminder message](/docs/api/rest/reference/chat/methods/#operation/createReminderForMessage) +- [Send a chat file](/docs/api/rest/reference/chat/methods/#operation/sendChatFile) +- [Send a chat message](/docs/api/rest/reference/chat/methods/#operation/sendaChatMessage) +- [React to a chat message](/docs/api/rest/reference/chat/methods/#operation/reactMessage) +- [Get a message](/docs/api/rest/reference/chat/methods/#operation/getChatMessage) +- [List bookmarks](/docs/api/rest/reference/chat/methods/#operation/fetchBookmarks) +- [Add or remove a bookmark](/docs/api/rest/reference/chat/methods/#operation/addOrRemoveABookmark) +- [Update a message](/docs/api/rest/reference/chat/methods/#operation/editMessage) +- [Delete a reminder for a message](/docs/api/rest/reference/chat/methods/#operation/deleteReminderForMessage) +- [Delete a scheduled message](/docs/api/rest/reference/chat/methods/#operation/deleteScheduleMessage) +- [Delete a message](/docs/api/rest/reference/chat/methods/#operation/deleteChatMessage) +- [Upload a chat file](/docs/api/rest/reference/chat/methods/#operation/uploadAChatFile) +- [List scheduled messages](/docs/api/rest/reference/chat/methods/#operation/listScheduledMessages) +- [List user's chat messages](/docs/api/rest/reference/chat/methods/#operation/getChatMessages) +- [Get a forwarded message](/docs/api/rest/reference/chat/methods/#operation/getForwardedMessage) + +### chat_message:write:admin + +View and manage all users' team chat messages + +**Associated APIs:** + +- [Get a forwarded message](/docs/api/rest/reference/chat/methods/#operation/getForwardedMessage) +- [Mark message read or unread](/docs/api/rest/reference/chat/methods/#operation/markMessage) +- [React to a chat message](/docs/api/rest/reference/chat/methods/#operation/reactMessage) +- [Delete a message](/docs/api/rest/reference/chat/methods/#operation/deleteChatMessage) +- [Send a chat message](/docs/api/rest/reference/chat/methods/#operation/sendaChatMessage) +- [Update a message](/docs/api/rest/reference/chat/methods/#operation/editMessage) +- [Get a message](/docs/api/rest/reference/chat/methods/#operation/getChatMessage) +- [Send a chat file](/docs/api/rest/reference/chat/methods/#operation/sendChatFile) +- [List user's chat messages](/docs/api/rest/reference/chat/methods/#operation/getChatMessages) +- [Upload a chat file](/docs/api/rest/reference/chat/methods/#operation/uploadAChatFile) + +### chat_migration:read:admin + +View information about a chat history migration + +**Associated APIs:** + +- [Get migrated Zoom user IDs](/docs/api/rest/reference/chat/methods/#operation/getMigrationUsersMapping) +- [Get migrated Zoom channel IDs](/docs/api/rest/reference/chat/methods/#operation/getMigrationChannelsMapping) + +### chat_migration:write:admin + +Migrate chat history from another vendor + +**Associated APIs:** + +- [Migrate channel members](/docs/api/rest/reference/chat/methods/#operation/MigrateChannelMembers) +- [Migrate 1:1 conversation or channel operations](/docs/api/rest/reference/chat/methods/#operation/Migrate1:1ConversationOrChannelOperations) +- [Migrate a chat channel](/docs/api/rest/reference/chat/methods/#operation/MigrateAChatChannel) +- [Migrate chat message reactions](/docs/api/rest/reference/chat/methods/#operation/MigrateChatMessageReactions) +- [Migrate chat messages](/docs/api/rest/reference/chat/methods/#operation/MigrateChatMessages) + +### imchat:bot + +Enable Chatbot within Zoom Team Chat Client + +**Associated APIs:** + +- [Delete a Chatbot message](/docs/api/rest/reference/chatbot/methods/#operation/deleteAChatbotMessage) +- [Send Chatbot messages](/docs/api/rest/reference/chatbot/methods/#operation/sendChatbot) +- [Link Unfurls](/docs/api/rest/reference/chatbot/methods/#operation/unfurlingLink) +- [Edit a Chatbot message](/docs/api/rest/reference/chatbot/methods/#operation/editChatbotMessage) + +### imchat:read:admin + +View all users history and channels + +**Associated APIs:** + +- [Get chat message reports](/docs/api/rest/reference/chat/methods/#operation/reportChatMessages) +- [Get chat sessions reports](/docs/api/rest/reference/chat/methods/#operation/reportChatSessions) + +### imchat:write + +Send a team chat message to a Zoom Team Chat user or channel + +**Associated APIs:** + +- [Send IM messages](/docs/api/rest/reference/chat/methods/#operation/sendimmessages) + +## Clips + +### clips:read + +View your clips information + +**Associated APIs:** + +- [List clip comments](/docs/api/rest/reference/clips/methods/#operation/Listclipcomments) +- [Get collaborators of a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipCollaborators) +- [Download a clip](/docs/api/rest/reference/clips/methods/#operation/downloadClip) +- [Get a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipById) +- [List all clips](/docs/api/rest/reference/clips/methods/#operation/GetUserClips) + +### clips:read:admin + +View your clips information + +**Associated APIs:** + +- [List clip comments](/docs/api/rest/reference/clips/methods/#operation/Listclipcomments) +- [Get collaborators of a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipCollaborators) +- [Download a clip](/docs/api/rest/reference/clips/methods/#operation/downloadClip) +- [Transfer task status check](/docs/api/rest/reference/clips/methods/#operation/Transfertaskstatuscheck) +- [Get a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipById) +- [List all clips](/docs/api/rest/reference/clips/methods/#operation/GetUserClips) + +### clips:write + +Manage your clips information + +**Associated APIs:** + +- [Delete a clip(soft delete)](/docs/api/rest/reference/clips/methods/#operation/DeleteClip) +- [Remove the collaborator from a clip](/docs/api/rest/reference/clips/methods/#operation/DeleteCollaborator) +- [Initiate and complete the multipart file upload for a clip](/docs/api/rest/reference/clips/methods/#operation/InitiateAndCompleteAClipMultipartUpload.) +- [Upload clip multipart files](/docs/api/rest/reference/clips/methods/#operation/UploadIqMultipartClipFile) +- [Delete a comment](/docs/api/rest/reference/clips/methods/#operation/Deleteacomment) +- [Upload clip file](/docs/api/rest/reference/clips/methods/#operation/UploadClipFile) + +### clips:write:admin + +Manage your clips information + +**Associated APIs:** + +- [Transfer clips owner](/docs/api/rest/reference/clips/methods/#operation/Transferclipsowner) +- [Remove the collaborator from a clip](/docs/api/rest/reference/clips/methods/#operation/DeleteCollaborator) +- [Initiate and complete the multipart file upload for a clip](/docs/api/rest/reference/clips/methods/#operation/InitiateAndCompleteAClipMultipartUpload.) +- [Upload clip file](/docs/api/rest/reference/clips/methods/#operation/UploadClipFile) +- [Delete a clip(soft delete)](/docs/api/rest/reference/clips/methods/#operation/DeleteClip) +- [Delete a comment](/docs/api/rest/reference/clips/methods/#operation/Deleteacomment) +- [Upload clip multipart files](/docs/api/rest/reference/clips/methods/#operation/UploadIqMultipartClipFile) + +## Conference Room Connector (CRC) + +### apiconnector:master + +This scope allows an app to view and manage sub account’s API Connector information + +**Associated APIs:** + +- [List API Connectors](/docs/api/rest/reference/crc/ma/#operation/GetListAPIConnectors) +- [Get an API Connector](/docs/api/rest/reference/crc/ma/#operation/GetAPIConnector) +- [Update an API Connector](/docs/api/rest/reference/crc/ma/#operation/UpdateAPIConnector) +- [Create an API Connector](/docs/api/rest/reference/crc/ma/#operation/CreateAPIConnector) +- [Update an API Connector's private key](/docs/api/rest/reference/crc/ma/#operation/UpdateAPIConnectorPrivateKey) +- [Get an API Connector's private key](/docs/api/rest/reference/crc/ma/#operation/GetanAPIConnector'sprivatekey) +- [Delete an API Connector](/docs/api/rest/reference/crc/ma/#operation/DeleteAPIConnector) + +### apiconnector:read:admin + +This scope allow an app to view all API Connector information + +**Associated APIs:** + +- [Get an API Connector](/docs/api/rest/reference/crc/methods/#operation/GetAPIConnector) +- [List API Connectors](/docs/api/rest/reference/crc/methods/#operation/GetListAPIConnectors) +- [Get an API Connector's private key](/docs/api/rest/reference/crc/methods/#operation/GetanAPIConnector'sprivatekey) + +### apiconnector:write:admin + +This scope allow an app to view and manage all API Connector information + +**Associated APIs:** + +- [Update an API Connector](/docs/api/rest/reference/crc/methods/#operation/UpdateAPIConnector) +- [Delete an API Connector](/docs/api/rest/reference/crc/methods/#operation/DeleteAPIConnector) +- [Update an API Connector's private key](/docs/api/rest/reference/crc/methods/#operation/UpdateAPIConnectorPrivateKey) +- [Create an API Connector](/docs/api/rest/reference/crc/methods/#operation/CreateAPIConnector) + +### crc:master + +View and manage sub account's CRC configuration + +**Associated APIs:** + +- [Get participant identifier code](/docs/api/rest/reference/crc/methods/#operation/get_participant_identifier_code) + +### crc:read:admin + +View and manage CRC configuration + +**Associated APIs:** + +- [Get participant identifier code](/docs/api/rest/reference/crc/methods/#operation/get_participant_identifier_code) + +### crc_account:master + +View and manage all sub account's Cisco/Polycom room account setting + +**Associated APIs:** + +- [Get Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/ma/#operation/getCiscoPolycomRoomAccountSetting) +- [Update Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/ma/#operation/UpdateCiscoPolycomRoomAccountSetting) + +### crc_account:read:admin + +View Cisco/Polycom rooms account setting + +**Associated APIs:** + +- [Get Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/methods/#operation/getCiscoPolycomRoomAccountSetting) + +### crc_account:write:admin + +View and manage Cisco/Polycom rooms account setting + +**Associated APIs:** + +- [Update Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/methods/#operation/UpdateCiscoPolycomRoomAccountSetting) + +### crc_rooms:master + +View and manage sub account’s managed Cisco and Polycom rooms information + +**Associated APIs:** + +- [Delete a room template](/docs/api/rest/reference/crc/ma/#operation/Deletearoomtemplate) +- [Update a Managed Room](/docs/api/rest/reference/crc/ma/#operation/UpdateaManagedRoom) +- [Update a Room Template](/docs/api/rest/reference/crc/ma/#operation/UpdateaRoomTemplate) +- [Create a Managed Room](/docs/api/rest/reference/crc/ma/#operation/CreateaManagedRoom) +- [Get a Managed Room](/docs/api/rest/reference/crc/ma/#operation/GetaManagedRoom) +- [Get a Room Template](/docs/api/rest/reference/crc/ma/#operation/GetaRoomTemplate) +- [List Room Templates](/docs/api/rest/reference/crc/ma/#operation/ListRoomTemplates) +- [Delete a managed room](/docs/api/rest/reference/crc/ma/#operation/Deleteamanagedroom) +- [List Managed Rooms](/docs/api/rest/reference/crc/ma/#operation/ListManagedRooms) +- [Create a Room Template](/docs/api/rest/reference/crc/ma/#operation/CreateaRoomTemplate) + +### crc_rooms:read:admin + +View all managed Cisco and Polycom rooms information + +**Associated APIs:** + +- [Get a Room Template](/docs/api/rest/reference/crc/methods/#operation/GetaRoomTemplate) +- [List Managed Rooms](/docs/api/rest/reference/crc/methods/#operation/ListManagedRooms) +- [List Room Templates](/docs/api/rest/reference/crc/methods/#operation/ListRoomTemplates) +- [Get a Managed Room](/docs/api/rest/reference/crc/methods/#operation/GetaManagedRoom) + +### crc_rooms:write:admin + +View and manage all managed Cisco and Polycom rooms information + +**Associated APIs:** + +- [Delete a room template](/docs/api/rest/reference/crc/methods/#operation/Deletearoomtemplate) +- [Update a Room Template](/docs/api/rest/reference/crc/methods/#operation/UpdateaRoomTemplate) +- [Create a Room Template](/docs/api/rest/reference/crc/methods/#operation/CreateaRoomTemplate) +- [Create a Managed Room](/docs/api/rest/reference/crc/methods/#operation/CreateaManagedRoom) +- [Delete a managed room](/docs/api/rest/reference/crc/methods/#operation/Deleteamanagedroom) +- [Update a Managed Room](/docs/api/rest/reference/crc/methods/#operation/UpdateaManagedRoom) + +## Contact Center + +### contact_center_attachment:read:admin + +View your contact center attachment information + +**Associated APIs:** + +- [Get an engagement's attachments](/docs/api/rest/reference/contact-center/methods/#operation/ListAttachments) + +### contact_center_flow:write:admin + +flow write + +**Associated APIs:** + +- [Delete a flow](/docs/api/rest/reference/contact-center/methods/#operation/DeleteFlow) +- [Edit a flow](/docs/api/rest/reference/contact-center/methods/#operation/EditFlow) +- [Publish a flow](/docs/api/rest/reference/contact-center/methods/#operation/PublishFlow) +- [Remove flow entry points](/docs/api/rest/reference/contact-center/methods/#operation/RemoveFlowEntryPoints) +- [Import a flow](/docs/api/rest/reference/contact-center/methods/#operation/ImportFlow) +- [Add flow entry points](/docs/api/rest/reference/contact-center/methods/#operation/AddFlowEntryPoints) + +### contact_center_messaging:read:admin + +Get message + +**Associated APIs:** + +- [List work item message history](/docs/api/rest/reference/contact-center/methods/#operation/getWorkItemMessageHistory) +- [List email message history](/docs/api/rest/reference/contact-center/methods/#operation/getEmailMessageHistory) +- [List message history](/docs/api/rest/reference/contact-center/methods/#operation/getMessageHistory) + +### contact_center_messaging:write:admin + +send message + +**Associated APIs:** + +- [Send a message](/docs/api/rest/reference/contact-center/methods/#operation/SendaMessage) + +### contact_center_routing_profile:write:admin + +Edit agent routing profile. + +**Associated APIs:** + +- [Update a consumer routing profile's details](/docs/api/rest/reference/contact-center/methods/#operation/Updateaconsumerroutingprofile'sdetails) +- [Delete an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Deleteanagentroutingprofile) +- [Create an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Createanagentroutingprofile) +- [Update an agent routing profile's details](/docs/api/rest/reference/contact-center/methods/#operation/Updateanagentroutingprofile'sdetails) +- [Delete a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Deleteaconsumerroutingprofile) +- [Create a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Createaconsumerroutingprofile) + +### contact_center_team:read:admin + +View your contact center team information + +**Associated APIs:** + +- [List teams](/docs/api/rest/reference/contact-center/methods/#operation/listTeams) +- [List team's parent teams](/docs/api/rest/reference/contact-center/methods/#operation/getTeamParentTeams) +- [Get a team](/docs/api/rest/reference/contact-center/methods/#operation/getTeamDetail) +- [List team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/listTeamSupervisors) +- [List a team's child teams](/docs/api/rest/reference/contact-center/methods/#operation/getTeamChildTeams) +- [List team agents](/docs/api/rest/reference/contact-center/methods/#operation/listTeamAgents) + +### contact_center_team:write:admin + +Manage your contact center team information + +**Associated APIs:** + +- [Create a team](/docs/api/rest/reference/contact-center/methods/#operation/CreateTeam) +- [Assign team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/assignTeamSupervisors) +- [Assign team agents](/docs/api/rest/reference/contact-center/methods/#operation/assignTeamAgents) +- [Delete a team](/docs/api/rest/reference/contact-center/methods/#operation/deleteTeam) +- [Unassign team agents](/docs/api/rest/reference/contact-center/methods/#operation/unassignTeamAgents) +- [Update a team](/docs/api/rest/reference/contact-center/methods/#operation/Updateateam) +- [Move a team](/docs/api/rest/reference/contact-center/methods/#operation/moveTeam) +- [Unassign team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/unassignTeamSupervisors) + +### contact_center_user_template:read:admin + +View all contact center user template’s details. + +**Associated APIs:** + +- [Get a user template](/docs/api/rest/reference/contact-center/methods/#operation/Getanusertemplate) + +### contact_center_user_template:write:admin + +View and manage all contact center user template's details. + +**Associated APIs:** + +- [Update a user template](/docs/api/rest/reference/contact-center/methods/#operation/updateAUserTemplate) +- [Create a user template](/docs/api/rest/reference/contact-center/methods/#operation/createAUserTemplate) +- [Delete a user template](/docs/api/rest/reference/contact-center/methods/#operation/deleteAUserTemplate) + +## ContactCenter + +### contact_center_asset_library:read:admin + +View your contact center asset library information + +**Associated APIs:** + +- [List assets](/docs/api/rest/reference/contact-center/methods/#operation/listAssets) +- [List asset categories](/docs/api/rest/reference/contact-center/methods/#operation/listAssetCategories) +- [Get an asset](/docs/api/rest/reference/contact-center/methods/#operation/getAnAsset) +- [Get an asset category](/docs/api/rest/reference/contact-center/methods/#operation/getAnAssetCategory) + +### contact_center_asset_library:write:admin + +Manage your contact center asset library information + +**Associated APIs:** + +- [Create an asset](/docs/api/rest/reference/contact-center/methods/#operation/createAnAsset) +- [Delete an asset](/docs/api/rest/reference/contact-center/methods/#operation/deleteAnAsset) +- [Update an asset](/docs/api/rest/reference/contact-center/methods/#operation/updateAnAsset) +- [Delete an asset category](/docs/api/rest/reference/contact-center/methods/#operation/deleteAnAssetCategory) +- [Create an asset category](/docs/api/rest/reference/contact-center/methods/#operation/createAnAssetCategory) +- [Update an asset category](/docs/api/rest/reference/contact-center/methods/#operation/updateAnAssetCategory) +- [Delete asset items](/docs/api/rest/reference/contact-center/methods/#operation/Deleteassetitems) +- [Duplicate an asset](/docs/api/rest/reference/contact-center/methods/#operation/duplicateAnAsset) + +### contact_center_contact:read:admin + +View address book information + +**Associated APIs:** + +- [List address books](/docs/api/rest/reference/contact-center/methods/#operation/listAddressBooks) +- [Get an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/getUnit) +- [List address book units](/docs/api/rest/reference/contact-center/methods/#operation/listUnits) +- [List a contact's custom fields](/docs/api/rest/reference/contact-center/methods/#operation/ListContactCustomFields) +- [List an address book's custom fields](/docs/api/rest/reference/contact-center/methods/#operation/Listaddressbookcustomfields) +- [Get an address book](/docs/api/rest/reference/contact-center/methods/#operation/getAddressBook) +- [Get an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/getContact) +- [List address book contacts](/docs/api/rest/reference/contact-center/methods/#operation/listContacts) +- [Get an address book's custom field](/docs/api/rest/reference/contact-center/methods/#operation/Getaaddressbookcustomfield) + +### contact_center_contact:write:admin + +Update address book information + +**Associated APIs:** + +- [Update an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/updateUnit) +- [Update an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/updateContact) +- [Update an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Updateacustomfield) +- [Create an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/createContact) +- [Create an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/createUnit) +- [Create an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Createacustomfield) +- [Delete an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/contactDelete) +- [Delete an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Deleteancustomfield) +- [Delete an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/deleteUnit) +- [Delete an address book](/docs/api/rest/reference/contact-center/methods/#operation/deleteAddressBook) +- [Create an address book](/docs/api/rest/reference/contact-center/methods/#operation/createAddressBook) +- [Update an address book](/docs/api/rest/reference/contact-center/methods/#operation/updateAddressBook) + +### contact_center_disposition:read:admin + +View your contact center disposition information + +**Associated APIs:** + +- [Get a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/getSet) +- [List disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/listSets) +- [Get a disposition](/docs/api/rest/reference/contact-center/methods/#operation/getDisposition) +- [List dispositions](/docs/api/rest/reference/contact-center/methods/#operation/listDispositions) + +### contact_center_disposition:write:admin + +Manage your contact center disposition information + +**Associated APIs:** + +- [Create a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/createSet) +- [Delete a disposition](/docs/api/rest/reference/contact-center/methods/#operation/deleteDisposition) +- [Update a disposition](/docs/api/rest/reference/contact-center/methods/#operation/updateDisposition) +- [Delete a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/deleteSet) +- [Update a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/updateSet) +- [Create a disposition](/docs/api/rest/reference/contact-center/methods/#operation/createDisposition) + +### contact_center_engagement:read:admin + +View all contact center engagement information + +**Associated APIs:** + +- [Poll an engagement recording's status](/docs/api/rest/reference/contact-center/methods/#operation/EngagementRecordingStatus) +- [Get an engagement's survey](/docs/api/rest/reference/contact-center/methods/#operation/getEngagementSurvey) + +### contact_center_engagement:write:admin + +Manage all contact center engagement information + +**Associated APIs:** + +- [Control an engagement's recording](/docs/api/rest/reference/contact-center/methods/#operation/engagementRecordingControl) +- [Update an engagement](/docs/api/rest/reference/contact-center/methods/#operation/updateEngagement) +- [Start an engagement](/docs/api/rest/reference/contact-center/methods/#operation/Startworkitemengagement) + +### contact_center_flow:read:admin + +View all contact center flow information + +**Associated APIs:** + +- [List the closures' flows](/docs/api/rest/reference/contact-center/methods/#operation/listClosureSetFlows) +- [List flows](/docs/api/rest/reference/contact-center/methods/#operation/listFlows) +- [Export a flow](/docs/api/rest/reference/contact-center/methods/#operation/ExportFlow) +- [List entry points](/docs/api/rest/reference/contact-center/methods/#operation/ListentryPoints) +- [List flow's entry points](/docs/api/rest/reference/contact-center/methods/#operation/ListFlowEntryPoints) +- [Get a flow](/docs/api/rest/reference/contact-center/methods/#operation/getAFlow) +- [List the business hours' flows](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHourFlows) + +### contact_center_inbox:read:admin + +View all contact center inbox information + +**Associated APIs:** + +- [List an inbox's messages](/docs/api/rest/reference/contact-center/methods/#operation/listInboxMessages) +- [Get an inbox](/docs/api/rest/reference/contact-center/methods/#operation/getInbox) +- [List inboxes](/docs/api/rest/reference/contact-center/methods/#operation/listInbox) +- [Get inbox email notification list](/docs/api/rest/reference/contact-center/methods/#operation/Getinboxemailnotificationlist) +- [List an account's inbox messages](/docs/api/rest/reference/contact-center/methods/#operation/listInboxesMessages) + +### contact_center_inbox:write:admin + +Manage all contact center inbox information + +**Associated APIs:** + +- [Update an inbox](/docs/api/rest/reference/contact-center/methods/#operation/inboxUpdate) +- [Delete an inbox's messages](/docs/api/rest/reference/contact-center/methods/#operation/inboxMessagesDelete) +- [Delete inboxes](/docs/api/rest/reference/contact-center/methods/#operation/inboxesDelete) +- [Update an inbox email notification](/docs/api/rest/reference/contact-center/methods/#operation/Updateaninboxemailnotification) +- [Create an inbox](/docs/api/rest/reference/contact-center/methods/#operation/inboxCreate) +- [Delete inbox messages](/docs/api/rest/reference/contact-center/methods/#operation/inboxesMessagesDelete) +- [Delete an inbox message](/docs/api/rest/reference/contact-center/methods/#operation/inboxMessageDelete) + +### contact_center_note:read:admin + +View all contact center note information + +**Associated APIs:** + +- [List notes](/docs/api/rest/reference/contact-center/methods/#operation/notes) +- [Get a note](/docs/api/rest/reference/contact-center/methods/#operation/getNote) +- [List engagement notes](/docs/api/rest/reference/contact-center/methods/#operation/engagementNotes) + +### contact_center_note:write + +Manage all contact center note information + +**Associated APIs:** + +- [Update a note](/docs/api/rest/reference/contact-center/methods/#operation/noteUpdate) + +### contact_center_operating_hours:read:admin + +View all contact center operating hours information + +**Associated APIs:** + +- [List business hours](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHours) +- [Get a closure set](/docs/api/rest/reference/contact-center/methods/#operation/getAClosureSet) +- [List closures](/docs/api/rest/reference/contact-center/methods/#operation/listClosures) +- [Get business hours](/docs/api/rest/reference/contact-center/methods/#operation/getABusinessHour) + +### contact_center_operating_hours:write:admin + +Manage all contact center operating hours information + +**Associated APIs:** + +- [Create business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourCreate) +- [Delete business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourDelete) +- [Update business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourUpdate) +- [Create a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closuresSetCreate) +- [Delete a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closureSetDelete) +- [Update a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closureSetUpdate) + +### contact_center_operation_logs:read:admin + +Read permission for Operation logs + +**Associated APIs:** + +- [List operation logs](/docs/api/rest/reference/contact-center/methods/#operation/listOperationLogs) + +### contact_center_outbound_campaign:delete:admin + +Delete an Outbound Campaign. + +**Associated APIs:** + +- [Delete an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/deleteOutboundCampaign) + +### contact_center_outbound_campaign:read:admin + +View an Outbound Campaign. + +**Associated APIs:** + +- [List outbound campaigns](/docs/api/rest/reference/contact-center/methods/#operation/listOutboundCampaigns) +- [Get an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/getOutboundCampaign) + +### contact_center_outbound_campaign:update:admin + +Update an Outbound Campaign + +**Associated APIs:** + +- [Update an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/updateOutboundCampaign) + +### contact_center_outbound_campaign:write:admin + +Create an Outbound Campaign + +**Associated APIs:** + +- [Update an outbound campaign status](/docs/api/rest/reference/contact-center/methods/#operation/Updateanoutboundcampaignstatus) +- [Create an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/createOutboundCampaign) + +### contact_center_outbound_campaign_contactlist:read:admin + +Get campaign contact list + +**Associated APIs:** + +- [Get a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/getCampaignContactList) +- [List campaign contact lists](/docs/api/rest/reference/contact-center/methods/#operation/listCampaignContactLists) + +### contact_center_outbound_campaign_contactlist:write:admin + +Create campaign contact list + +**Associated APIs:** + +- [Create a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/createCampaignContactList) +- [Update a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/updateCampaignContactList) +- [Remove a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/deleteCampaignContactList) + +### contact_center_outbound_campaign_contacts:read:admin + +Get campaign contact list contact + +**Associated APIs:** + +- [Get a campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/getCampaignContactListContact) +- [List campaign contact list contacts](/docs/api/rest/reference/contact-center/methods/#operation/listCampaignContactListContacts) + +### contact_center_outbound_campaign_contacts:write:admin + +Create campaign contact list contact + +**Associated APIs:** + +- [Create a campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/createCampaignContactListContact) +- [Update contact on a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/updateCampaignContactListContact) +- [Remove campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/deleteCampaigncontactListContact) + +### contact_center_preference:read:admin + +View your contact center preference information + +**Associated APIs:** + +- [List system statuses](/docs/api/rest/reference/contact-center/methods/#operation/listSystemStatus) +- [Get a system status](/docs/api/rest/reference/contact-center/methods/#operation/getAStatus) + +### contact_center_preference:write:admin + +Manage your contact center preference information + +**Associated APIs:** + +- [Update a system status](/docs/api/rest/reference/contact-center/methods/#operation/updateSystemStatus) +- [Create a system status](/docs/api/rest/reference/contact-center/methods/#operation/createSystemStatus) +- [Delete a system status](/docs/api/rest/reference/contact-center/methods/#operation/deleteSystemStatus) + +### contact_center_queue:read:admin + +View your contact center queue information + +**Associated APIs:** + +- [List a queue's scheduled callbacks availability](/docs/api/rest/reference/contact-center/methods/#operation/Listqueuescheduledcallbacksavailability) +- [List queue templates](/docs/api/rest/reference/contact-center/methods/#operation/Listqueuetemplates) +- [List queue disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/getQueueDispositionSets) +- [Get a closure set](/docs/api/rest/reference/contact-center/methods/#operation/getAClosureSet) +- [List queue supervisors](/docs/api/rest/reference/contact-center/methods/#operation/getQueueSupervisors) +- [List closures](/docs/api/rest/reference/contact-center/methods/#operation/listClosures) +- [List queue dispositions](/docs/api/rest/reference/contact-center/methods/#operation/getQueueDispositions) +- [List business hours](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHours) +- [List queue agents](/docs/api/rest/reference/contact-center/methods/#operation/getQueueAgents) +- [Get inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/listInboxQueues) +- [List the business hours' queues](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHourQueues) +- [Get a queue](/docs/api/rest/reference/contact-center/methods/#operation/getAQueue) +- [List the closures' queues](/docs/api/rest/reference/contact-center/methods/#operation/listClosureSetQueues) +- [Get a queue's operating hours](/docs/api/rest/reference/contact-center/methods/#operation/getAQueueOperatingHours) +- [Get business hours](/docs/api/rest/reference/contact-center/methods/#operation/getABusinessHour) +- [List queues](/docs/api/rest/reference/contact-center/methods/#operation/listQueues) + +### contact_center_queue:write:admin + +Manage your contact center queue information + +**Associated APIs:** + +- [Unassign a queue team](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueTeam) +- [Delete a queue's interrupt menu configuration](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueInterruptMenu) +- [Unassign multiple teams in a queue](/docs/api/rest/reference/contact-center/methods/#operation/batchDeleteQueueTeams) +- [Assign queue dispositions](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueDispositions) +- [Batch create queues with a template](/docs/api/rest/reference/contact-center/methods/#operation/Batchcreatequeueswithatemplate) +- [Unassign a queue agent](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueAgent) +- [Update a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueUpdate) +- [Update a queue's operating hours](/docs/api/rest/reference/contact-center/methods/#operation/QueueOperatingHoursUpdate) +- [Create business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourCreate) +- [Unassign a queue supervisor](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueSupervisor) +- [Batch delete queues](/docs/api/rest/reference/contact-center/methods/#operation/Batchdeletequeues) +- [Assign queue teams](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueTeams) +- [Unassign a queue disposition set](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueDispositionSet) +- [Remove inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/unassignInboxQueues) +- [Update a queue's interrupt settings](/docs/api/rest/reference/contact-center/methods/#operation/updateQueueInterrupts) +- [Assign inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/assignInboxQueues) +- [Delete an attendee from a scheduled callback event](/docs/api/rest/reference/contact-center/methods/#operation/Deleteascheduledcallbackforanattendee) +- [Schedule a callback on a queue](/docs/api/rest/reference/contact-center/methods/#operation/Scheduleacallbackonaqueue) +- [Unassign a queue disposition](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueDisposition) +- [Update a queue agent](/docs/api/rest/reference/contact-center/methods/#operation/updateQueueAgent) +- [Delete a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueDelete) +- [Create a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closuresSetCreate) +- [Create a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueCreate) +- [Assign queue disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueDispositionSets) +- [Assign queue agents](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueAgents) +- [Assign a queue menu based interrupt](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueMenuBasedInterrupt) +- [Assign queue supervisors](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueSupervisors) + +### contact_center_recording:read + +View your contact center recording information. + +**Associated APIs:** + +- [List queue recordings](/docs/api/rest/reference/contact-center/methods/#operation/listQueueRecordings) +- [List a user's recordings](/docs/api/rest/reference/contact-center/methods/#operation/listUserRecordings) + +### contact_center_recording:read:admin + +View all contact center recording information + +**Associated APIs:** + +- [List recordings](/docs/api/rest/reference/contact-center/methods/#operation/listRecordings) +- [List a user's recordings](/docs/api/rest/reference/contact-center/methods/#operation/listUserRecordings) +- [List queue recordings](/docs/api/rest/reference/contact-center/methods/#operation/listQueueRecordings) +- [List engagement recordings](/docs/api/rest/reference/contact-center/methods/#operation/listEngagementRecordings) + +### contact_center_recording:write:admin + +Manage all contact center recording information + +**Associated APIs:** + +- [Delete a recording](/docs/api/rest/reference/contact-center/methods/#operation/deleteRecording) +- [Delete engagement recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteEngagementRecordings) +- [Delete queue recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueRecordings) +- [Delete a user's recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteUserRecordings) + +### contact_center_region:read:admin + +View all contact center region information + +**Associated APIs:** + +- [Get a region](/docs/api/rest/reference/contact-center/methods/#operation/GetARegion) +- [List regions](/docs/api/rest/reference/contact-center/methods/#operation/ListRegions) +- [List a region's users](/docs/api/rest/reference/contact-center/methods/#operation/ListRegion'sUsers) + +### contact_center_region:write:admin + +Manage all contact center region information + +**Associated APIs:** + +- [Delete a region](/docs/api/rest/reference/contact-center/methods/#operation/DeleteARegion) +- [Update a region](/docs/api/rest/reference/contact-center/methods/#operation/UpdateARegion) +- [Create a region](/docs/api/rest/reference/contact-center/methods/#operation/CreateARegion) +- [Assign users to a region](/docs/api/rest/reference/contact-center/methods/#operation/AssignUsersToARegion) + +### contact_center_report:read:admin + +View all contact center report information + +**Associated APIs:** + +- [List historical queue's agents reports](/docs/api/rest/reference/contact-center/methods/#operation/listQueueAgentMetric) +- [List agent's status history reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentStatusHistory) +- [Get an engagement's events](/docs/api/rest/reference/contact-center/methods/#operation/getEngagementEvents) +- [List historical outbound dialer performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricaloutbounddialerperformancedatasetdata) +- [List historical disposition dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricaldispositiondatasetdata) +- [Get an engagement](/docs/api/rest/reference/contact-center/methods/#operation/getEngagement) +- [List historical queue reports](/docs/api/rest/reference/contact-center/methods/#operation/listHistoricalQueueMetric) +- [List historical engagement dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listengagementdatasetdata) +- [List historical detail reports](/docs/api/rest/reference/contact-center/methods/#operation/listHistoricalDetailMetric) +- [List historical queue performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalqueueperformancedatasetdata) +- [List historical flow performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalflowperformancedatasetdata) +- [List historical agent timecard dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalagenttimecarddatasetdata) +- [List agent leg reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentLegMetric) +- [List agent's time sheet reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentTimeSheet) +- [List historical agent reports by queue](/docs/api/rest/reference/contact-center/methods/#operation/listQueueAgentsMetrics) +- [List engagements](/docs/api/rest/reference/contact-center/methods/#operation/listEngagements) +- [List historical Zoom Phone to Contact Center call journey data](/docs/api/rest/reference/contact-center/methods/#operation/ListhistoricalZoomphonetozcccalljourneydata) +- [List historical engagement log data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalengagementlogs) +- [List historical agent performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalagentperformancedatasetdata) + +### contact_center_role:read:admin + +View your contact center role information + +**Associated APIs:** + +- [Get a role](/docs/api/rest/reference/contact-center/methods/#operation/getRole) +- [List users of a role](/docs/api/rest/reference/contact-center/methods/#operation/getRoleUsers) +- [List roles](/docs/api/rest/reference/contact-center/methods/#operation/listRoles) + +### contact_center_role:write:admin + +Manage your contact center role information + +**Associated APIs:** + +- [Delete a role](/docs/api/rest/reference/contact-center/methods/#operation/deleteRole) +- [Assign a role](/docs/api/rest/reference/contact-center/methods/#operation/assignRoleUsers) +- [Delete role privileges](/docs/api/rest/reference/contact-center/methods/#operation/Deleteroleprivileges) +- [Update a role](/docs/api/rest/reference/contact-center/methods/#operation/updateRole) +- [Create a role](/docs/api/rest/reference/contact-center/methods/#operation/createRole) +- [Unassign a role](/docs/api/rest/reference/contact-center/methods/#operation/deleteRoleUser) +- [Duplicate a role](/docs/api/rest/reference/contact-center/methods/#operation/Duplicatearole) + +### contact_center_routing_profile:read:admin + +View routing profile information. + +**Associated APIs:** + +- [Get a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Getaconsumerroutingprofile) +- [List agent routing profiles](/docs/api/rest/reference/contact-center/methods/#operation/Listagentroutingprofiles) +- [Get an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/getAgentRoutingProfile) +- [List consumer routing profiles](/docs/api/rest/reference/contact-center/methods/#operation/Listconsumerroutingprofiles) + +### contact_center_skill:read:admin + +View all contact center skill information + +**Associated APIs:** + +- [List user's skills](/docs/api/rest/reference/contact-center/methods/#operation/ListAUserSkills) +- [Get a skill](/docs/api/rest/reference/contact-center/methods/#operation/getSkill) +- [Get a skill category](/docs/api/rest/reference/contact-center/methods/#operation/getSkillCategory) +- [List skills](/docs/api/rest/reference/contact-center/methods/#operation/listSkills) +- [List skill categories](/docs/api/rest/reference/contact-center/methods/#operation/listSkillCategory) + +### contact_center_skill:write:admin + +Manage all contact center skill information + +**Associated APIs:** + +- [Update a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryUpdate) +- [Create a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryCreate) +- [Delete a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryDelete) +- [Update a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillNameUpdate) +- [Assign user's skills](/docs/api/rest/reference/contact-center/methods/#operation/assignSkills) +- [Create a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillCreate) +- [Delete a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillDelete) +- [Unassign user's skill](/docs/api/rest/reference/contact-center/methods/#operation/deleteASkill) + +### contact_center_sms:master + +View sub account’s SMS log information + +**Associated APIs:** + +- [List SMS logs](/docs/api/rest/reference/contact-center/ma/#operation/listSMS) + +### contact_center_sms:read:admin + +View all contact center SMS log information + +**Associated APIs:** + +- [List SMS logs](/docs/api/rest/reference/contact-center/methods/#operation/listSMS) + +### contact_center_user:read:admin + +View all contact center user’s details + +**Associated APIs:** + +- [List users of a skill](/docs/api/rest/reference/contact-center/methods/#operation/listSkillUsers) +- [List user's devices](/docs/api/rest/reference/contact-center/methods/#operation/Listuserdevices) +- [List users' profiles](/docs/api/rest/reference/contact-center/methods/#operation/users) +- [Get a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userGet) +- [List user's queues](/docs/api/rest/reference/contact-center/methods/#operation/listUserQueues) +- [List user templates](/docs/api/rest/reference/contact-center/methods/#operation/ListUserTemplates) +- [Get an inbox's users](/docs/api/rest/reference/contact-center/methods/#operation/listInboxUsers) + +### contact_center_user:write:admin + +View and manage all contact center user's details + +**Associated APIs:** + +- [Update a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userUpdate) +- [Assign inbox access users](/docs/api/rest/reference/contact-center/methods/#operation/assignInboxUsers) +- [Batch update user status](/docs/api/rest/reference/contact-center/methods/#operation/Batchupdateuserstatus) +- [Unassign inbox access users](/docs/api/rest/reference/contact-center/methods/#operation/unassignInboxUsers) +- [Batch delete user profiles](/docs/api/rest/reference/contact-center/methods/#operation/batchDeleteUsers) +- [Create a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/createUser) +- [Batch create user profiles](/docs/api/rest/reference/contact-center/methods/#operation/BatchCreateUsers) +- [Batch update user profiles](/docs/api/rest/reference/contact-center/methods/#operation/BatchUpdateUsers) +- [Update a user's status](/docs/api/rest/reference/contact-center/methods/#operation/Updateauser'sstatus) +- [Delete a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userDelete) +- [Command control of a user](/docs/api/rest/reference/contact-center/methods/#operation/userControl) + +### contact_center_variable:read:admin + +View all contact center variable information + +**Associated APIs:** + +- [Get a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableGet) +- [Get a variable group](/docs/api/rest/reference/contact-center/methods/#operation/getAVariableGroup) +- [List variable groups](/docs/api/rest/reference/contact-center/methods/#operation/listVariableGroups) +- [List variables](/docs/api/rest/reference/contact-center/methods/#operation/variables) + +### contact_center_variable:write:admin + +Manage all contact center variable information + +**Associated APIs:** + +- [Create a variable](/docs/api/rest/reference/contact-center/methods/#operation/createVariable) +- [Create a variable group](/docs/api/rest/reference/contact-center/methods/#operation/createVariableGroup) +- [Update a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableUpdate) +- [Update a variable group](/docs/api/rest/reference/contact-center/methods/#operation/updateVariableGroup) +- [Delete a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableDelete) +- [Delete a variable group](/docs/api/rest/reference/contact-center/methods/#operation/DeleteGroup) + +### contact_center_variable_log:read:admin + +View variable log information + +**Associated APIs:** + +- [List variable logs](/docs/api/rest/reference/contact-center/methods/#operation/listVariableLogs) +- [Get a variable log](/docs/api/rest/reference/contact-center/methods/#operation/getVariableLog) + +### contact_center_variable_log:write:admin + +Manage variable log information + +**Associated APIs:** + +- [Delete a variable log](/docs/api/rest/reference/contact-center/methods/#operation/deleteVariableLog) + +### contact_center_voice_call:master + +View sub account’s voice call information + +**Associated APIs:** + +- [List voice call logs](/docs/api/rest/reference/contact-center/ma/#operation/listVoiceCall) + +### contact_center_voice_call:read:admin + +view voice call information + +**Associated APIs:** + +- [List voice call logs](/docs/api/rest/reference/contact-center/methods/#operation/listVoiceCall) + +## Contacts + +### contact:read + +View current user's contacts + +**Associated APIs:** + +- [Search company contacts](/docs/api/rest/reference/chat/methods/#operation/searchCompanyContacts) + +### contact:read:admin + +View all users' contacts + +**Associated APIs:** + +- [Search company contacts](/docs/api/rest/reference/chat/methods/#operation/searchCompanyContacts) + +## Dashboard + +### dashboard:master + +View sub account's Dashboard data + +**Associated APIs:** + +- [Get issues of Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardIssueDetailZoomRoom) +- [List webinars](/docs/api/rest/reference/account/ma/#operation/dashboardWebinars) +- [Get meeting sharing/recording details](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantShare) +- [Get meeting quality scores](/docs/api/rest/reference/account/ma/#operation/dashboardQuality) +- [List client meeting satisfaction](/docs/api/rest/reference/account/ma/#operation/listMeetingSatisfaction) +- [Get CRC port usage](/docs/api/rest/reference/account/ma/#operation/dashboardCRC) +- [List meeting participants](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipants) +- [Get post meeting feedback](/docs/api/rest/reference/account/ma/#operation/participantFeedback) +- [List meeting participants QoS Summary](/docs/api/rest/reference/qss/ma/#operation/dashboardMeetingParticipantsQOSSummary) +- [Get chat metrics](/docs/api/rest/reference/account/ma/#operation/dashboardChat) +- [Get webinar participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantQOS) +- [Get Zoom Rooms details](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRoom) +- [Get webinar sharing/recording details](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantShare) +- [Get zoom meetings client feedback](/docs/api/rest/reference/account/ma/#operation/dashboardClientFeedbackDetail) +- [List the client versions](/docs/api/rest/reference/account/ma/#operation/getClientVersions) +- [Get webinar details](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarDetail) +- [Get webinar participants](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipants) +- [List webinar participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantsQOS) +- [List Zoom meetings client feedback](/docs/api/rest/reference/account/ma/#operation/dashboardClientFeedback) +- [List webinar participants QoS Summary](/docs/api/rest/reference/qss/ma/#operation/dashboardWebinarParticipantsQOSSummary) +- [List Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRooms) +- [Get meeting participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantQOS) +- [Get meeting details](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingDetail) +- [Get post webinar feedback](/docs/api/rest/reference/account/ma/#operation/participantWebinarFeedback) +- [Get top 25 issues of Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRoomIssue) +- [List meeting participants QoS](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantsQOS) +- [Get top 25 Zoom Rooms with issues](/docs/api/rest/reference/account/ma/#operation/dashboardIssueZoomRoom) + +### dashboard:read:admin + +View Dashboard data + +**Associated APIs:** + +- [Get issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardIssueDetailZoomRoom) +- [List webinars](/docs/api/rest/reference/account/methods/#operation/dashboardWebinars) +- [Get meeting sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantShare) +- [Get meeting quality scores](/docs/api/rest/reference/account/methods/#operation/dashboardQuality) +- [Get webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantQOS) +- [Get chat metrics](/docs/api/rest/reference/account/methods/#operation/dashboardChat) +- [Get top 25 issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoomIssue) +- [Get meeting participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantQOS) +- [List Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRooms) +- [List meeting participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardMeetingParticipantsQOSSummary) +- [List meeting participants](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipants) +- [Get CRC port usage](/docs/api/rest/reference/account/methods/#operation/dashboardCRC) +- [List webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantsQOS) +- [List Zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedback) +- [List webinar participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardWebinarParticipantsQOSSummary) +- [List the client versions](/docs/api/rest/reference/account/methods/#operation/getClientVersions) +- [Get webinar details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarDetail) +- [Get webinar participants](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipants) +- [List meetings](/docs/api/rest/reference/account/methods/#operation/dashboardMeetings) +- [Get post webinar feedback](/docs/api/rest/reference/account/methods/#operation/participantWebinarFeedback) +- [Get meeting details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingDetail) +- [Get Zoom Rooms details](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoom) +- [Get webinar sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantShare) +- [Get zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedbackDetail) +- [List meeting participants QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantsQOS) +- [Get top 25 Zoom Rooms with issues](/docs/api/rest/reference/account/methods/#operation/dashboardIssueZoomRoom) +- [Get post meeting feedback](/docs/api/rest/reference/account/methods/#operation/participantFeedback) +- [List client meeting satisfaction](/docs/api/rest/reference/account/methods/#operation/listMeetingSatisfaction) + +### dashboard_crc:read:admin + +View all users' usage statistics of CRC + +**Associated APIs:** + +- [Get CRC port usage](/docs/api/rest/reference/account/methods/#operation/dashboardCRC) + +### dashboard_home:read:admin + +View overview of usage statistics for Meetings and Zoom Rooms + +**Associated APIs:** + +- [Get issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardIssueDetailZoomRoom) +- [Get top 25 Zoom Rooms with issues](/docs/api/rest/reference/account/methods/#operation/dashboardIssueZoomRoom) +- [List client meeting satisfaction](/docs/api/rest/reference/account/methods/#operation/listMeetingSatisfaction) +- [List Zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedback) +- [Get zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedbackDetail) +- [Get meeting quality scores](/docs/api/rest/reference/account/methods/#operation/dashboardQuality) +- [List the client versions](/docs/api/rest/reference/account/methods/#operation/getClientVersions) + +### dashboard_im:read:admin + +View all users' usage statistics of team chat messages and message types + +**Associated APIs:** + +- [Get chat metrics](/docs/api/rest/reference/account/methods/#operation/dashboardChat) + +### dashboard_meetings:read:admin + +View all users' meetings information on Dashboard + +**Associated APIs:** + +- [List meeting participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardMeetingParticipantsQOSSummary) +- [List meeting participants QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantsQOS) +- [List meeting participants](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipants) +- [Get post meeting feedback](/docs/api/rest/reference/account/methods/#operation/participantFeedback) +- [Get meeting sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantShare) +- [List meetings](/docs/api/rest/reference/account/methods/#operation/dashboardMeetings) +- [Get meeting details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingDetail) +- [Get meeting participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantQOS) + +### dashboard_webinars:read:admin + +View all users' webinar information on Dashboard + +**Associated APIs:** + +- [Get webinar participants](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipants) +- [Get webinar sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantShare) +- [List webinars](/docs/api/rest/reference/account/methods/#operation/dashboardWebinars) +- [List webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantsQOS) +- [Get webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantQOS) +- [Get post webinar feedback](/docs/api/rest/reference/account/methods/#operation/participantWebinarFeedback) +- [List webinar participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardWebinarParticipantsQOSSummary) +- [Get webinar details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarDetail) + +### dashboard_zr:read:admin + +View all users' Zoom Room usage statistics and information + +**Associated APIs:** + +- [Get Zoom Rooms details](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoom) +- [List Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRooms) +- [Get top 25 issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoomIssue) + +## Device + +### device:read:admin + +Manage device + +**Associated APIs:** + +- [Get Zoom Phone Appliance settings by user ID](/docs/api/rest/reference/zoom-api/methods/#operation/GetZpaDeviceListProfileSettingOfaUser) +- [Get device detail](/docs/api/rest/reference/zoom-api/methods/#operation/getDevice) +- [Get ZPA version info](/docs/api/rest/reference/zoom-api/methods/#operation/GetZpaVersioninfo) +- [Get ZDM group info](/docs/api/rest/reference/zoom-api/methods/#operation/Getzdmgroupinfo) +- [List devices](/docs/api/rest/reference/zoom-api/methods/#operation/listDevices) + +### device:write:admin + +View and manage device + +**Associated APIs:** + +- [Get device detail](/docs/api/rest/reference/zoom-api/methods/#operation/getDevice) +- [Assign a device to a user or commonarea](/docs/api/rest/reference/zoom-api/methods/#operation/Assigndevicetoauser/commonarea) +- [List devices](/docs/api/rest/reference/zoom-api/methods/#operation/listDevices) +- [Delete device](/docs/api/rest/reference/zoom-api/methods/#operation/deleteDevice) +- [Add new device](/docs/api/rest/reference/zoom-api/methods/#operation/addDevice) +- [Assign a device to a group](/docs/api/rest/reference/zoom-api/methods/#operation/assginGroup) +- [Change device](/docs/api/rest/reference/zoom-api/methods/#operation/updateDevice) +- [Upgrade ZPA firmware or app](/docs/api/rest/reference/zoom-api/methods/#operation/UpgradeZpas/app) +- [Delete ZPA device by vendor and mac address](/docs/api/rest/reference/zoom-api/methods/#operation/DeleteZpaDeviceByVendorAndMacAddress) +- [Change device association](/docs/api/rest/reference/zoom-api/methods/#operation/changeDeviceAssociation) + +## Devices (H323) + +### h323:master + +View and manage sub account's H.323 devices + +**Associated APIs:** + +- [Delete a H.323/SIP device](/docs/api/rest/reference/zoom-api/ma/#operation/deviceDelete) +- [Update a H.323/SIP device](/docs/api/rest/reference/zoom-api/ma/#operation/deviceUpdate) +- [List all H.323/SIP devices](/docs/api/rest/reference/zoom-api/ma/#operation/deviceList) +- [Create a H.323/SIP device](/docs/api/rest/reference/zoom-api/ma/#operation/deviceCreate) + +### h323:read:admin + +View all users' H.323 devices + +**Associated APIs:** + +- [List all H.323/SIP devices](/docs/api/rest/reference/zoom-api/methods/#operation/deviceList) + +### h323:write:admin + +View and manage all users' H.323 devices + +**Associated APIs:** + +- [Update a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceUpdate) +- [Delete a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceDelete) +- [Create a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceCreate) + +## Docs + +### docs:delete + +Delete a file + +**Associated APIs:** + +- [Delete a file](/docs/api/rest/reference/Docs/methods/#operation/DeleteFile) + +### docs:delete:admin + +Delete a file + +**Associated APIs:** + +- [Delete a file](/docs/api/rest/reference/Docs/methods/#operation/DeleteFile) + +### docs:read:admin + +Get basic info of the file + +**Associated APIs:** + +- [List all children of a file](/docs/api/rest/reference/Docs/methods/#operation/ListAllChildren) +- [Get metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/QueryFileMetadata) + +### docs:write:admin + +Edit basic info of a file + +**Associated APIs:** + +- [Create a new file](/docs/api/rest/reference/Docs/methods/#operation/CreateDoc) +- [Modify metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/ModifyMetadata) + +### docs_file_uploads:write + +Create a file upload + +**Associated APIs:** + +- [Create file upload for docs import or attachments](/docs/api/rest/reference/Docs/methods/#operation/Uploadfilefordocsimportorattachments) + +### docs_file_uploads:write:admin + +Create a file upload + +**Associated APIs:** + +- [Create file upload for docs import or attachments](/docs/api/rest/reference/Docs/methods/#operation/Uploadfilefordocsimportorattachments) + +### docs_import:read + +Get import task status + +**Associated APIs:** + +- [Get file import status](/docs/api/rest/reference/Docs/methods/#operation/Getdocsfileimportstatus) + +### docs_import:read:admin + +Get import task status + +**Associated APIs:** + +- [Get file import status](/docs/api/rest/reference/Docs/methods/#operation/Getdocsfileimportstatus) + +### docs_import:write + +Create new file by import + +**Associated APIs:** + +- [Create a new file by import](/docs/api/rest/reference/Docs/methods/#operation/Createanewfilebyimport) + +### docs_import:write:admin + +Create new file by import + +**Associated APIs:** + +- [Create a new file by import](/docs/api/rest/reference/Docs/methods/#operation/Createanewfilebyimport) + +## Group + +### group:master + +View and manage sub account's groups + +**Associated APIs:** + +- [Get locked settings](/docs/api/rest/reference/user/ma/#operation/getGroupLockSettings) +- [Get a group's webinar registration settings](/docs/api/rest/reference/user/ma/#operation/groupSettingsRegistration) +- [Update a group's webinar registration settings](/docs/api/rest/reference/user/ma/#operation/groupSettingsRegistrationUpdate) +- [Update locked settings](/docs/api/rest/reference/user/ma/#operation/groupLockedSettings) +- [List group members](/docs/api/rest/reference/user/ma/#operation/groupMembers) +- [Upload Virtual Background files](/docs/api/rest/reference/user/ma/#operation/uploadGroupVB) +- [Delete a group admin](/docs/api/rest/reference/user/ma/#operation/groupAdminsDelete) +- [Get a group](/docs/api/rest/reference/user/ma/#operation/group) +- [Get a group's settings](/docs/api/rest/reference/user/ma/#operation/getGroupSettings) +- [List group channels](/docs/api/rest/reference/user/ma/#operation/groupChannels) +- [Update a group member](/docs/api/rest/reference/user/ma/#operation/updateAGroupMember) +- [List groups](/docs/api/rest/reference/user/ma/#operation/groups) +- [Create a group](/docs/api/rest/reference/user/ma/#operation/groupCreate) +- [List group admins](/docs/api/rest/reference/user/ma/#operation/groupAdmins) +- [Update a group's settings](/docs/api/rest/reference/user/ma/#operation/updateGroupSettings) +- [Add group members](/docs/api/rest/reference/user/ma/#operation/groupMembersCreate) +- [Delete Virtual Background files](/docs/api/rest/reference/user/ma/#operation/delGroupVB) +- [Add group admins](/docs/api/rest/reference/user/ma/#operation/groupAdminsCreate) +- [Delete a group member](/docs/api/rest/reference/user/ma/#operation/groupMembersDelete) +- [Delete a group](/docs/api/rest/reference/user/ma/#operation/groupDelete) +- [Update a group](/docs/api/rest/reference/user/ma/#operation/groupUpdate) + +### group:read:admin + +View groups + +**Associated APIs:** + +- [List group members](/docs/api/rest/reference/user/methods/#operation/groupMembers) +- [List groups](/docs/api/rest/reference/user/methods/#operation/groups) +- [List group admins](/docs/api/rest/reference/user/methods/#operation/groupAdmins) +- [Get a group's webinar registration settings](/docs/api/rest/reference/user/methods/#operation/groupSettingsRegistration) +- [List group channels](/docs/api/rest/reference/user/methods/#operation/groupChannels) +- [Get locked settings](/docs/api/rest/reference/user/methods/#operation/getGroupLockSettings) +- [Get a group's settings](/docs/api/rest/reference/user/methods/#operation/getGroupSettings) +- [Get a group](/docs/api/rest/reference/user/methods/#operation/group) + +### group:write:admin + +View and manage groups + +**Associated APIs:** + +- [Get a group](/docs/api/rest/reference/user/methods/#operation/group) +- [Update a group](/docs/api/rest/reference/user/methods/#operation/groupUpdate) +- [List group members](/docs/api/rest/reference/user/methods/#operation/groupMembers) +- [Update a group's webinar registration settings](/docs/api/rest/reference/user/methods/#operation/groupSettingsRegistrationUpdate) +- [Add group members](/docs/api/rest/reference/user/methods/#operation/groupMembersCreate) +- [Delete a group admin](/docs/api/rest/reference/user/methods/#operation/groupAdminsDelete) +- [Get locked settings](/docs/api/rest/reference/user/methods/#operation/getGroupLockSettings) +- [List group admins](/docs/api/rest/reference/user/methods/#operation/groupAdmins) +- [Create a group](/docs/api/rest/reference/user/methods/#operation/groupCreate) +- [Update a group's settings](/docs/api/rest/reference/user/methods/#operation/updateGroupSettings) +- [List groups](/docs/api/rest/reference/user/methods/#operation/groups) +- [Get a group's settings](/docs/api/rest/reference/user/methods/#operation/getGroupSettings) +- [Delete a group](/docs/api/rest/reference/user/methods/#operation/groupDelete) +- [Delete Virtual Background files](/docs/api/rest/reference/user/methods/#operation/delGroupVB) +- [Add group admins](/docs/api/rest/reference/user/methods/#operation/groupAdminsCreate) +- [Upload Virtual Background files](/docs/api/rest/reference/user/methods/#operation/uploadGroupVB) +- [Get a group's webinar registration settings](/docs/api/rest/reference/user/methods/#operation/groupSettingsRegistration) +- [Delete a group member](/docs/api/rest/reference/user/methods/#operation/groupMembersDelete) +- [List group channels](/docs/api/rest/reference/user/methods/#operation/groupChannels) +- [Update a group member](/docs/api/rest/reference/user/methods/#operation/updateAGroupMember) +- [Update locked settings](/docs/api/rest/reference/user/methods/#operation/groupLockedSettings) + +## IM Group + +### contact_group:read:admin + +View all contact group information + +**Associated APIs:** + +- [List contact groups](/docs/api/rest/reference/user/methods/#operation/contactGroups) +- [List contact group members](/docs/api/rest/reference/user/methods/#operation/contactGroupMembers) + +### contact_group:write:admin + +Manage all contact group information + +**Associated APIs:** + +- [Add contact group members](/docs/api/rest/reference/user/methods/#operation/contactGroupMemberAdd) +- [Remove members in a contact group](/docs/api/rest/reference/user/methods/#operation/contactGroupMemberRemove) +- [Get a contact group](/docs/api/rest/reference/user/methods/#operation/contactGroup) +- [Update a contact group](/docs/api/rest/reference/user/methods/#operation/contactGroupUpdate) +- [Delete a contact group](/docs/api/rest/reference/user/methods/#operation/contactGroupDelete) +- [Create a contact group](/docs/api/rest/reference/user/methods/#operation/contactGroupCreate) + +### imgroup:master + +View and manage sub account's Zoom Team Chat Groups + +**Associated APIs:** + +- [Retrieve an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroup) +- [List IM directory groups](/docs/api/rest/reference/chat/ma/#operation/imGroups) +- [Add IM directory group members](/docs/api/rest/reference/chat/ma/#operation/imGroupMembersCreate) +- [Delete an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupDelete) +- [Update an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupUpdate) +- [Delete IM directory group member](/docs/api/rest/reference/chat/ma/#operation/imGroupMembersDelete) +- [List IM directory group members](/docs/api/rest/reference/chat/ma/#operation/imGroupMembers) +- [Create an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupCreate) + +### imgroup:read:admin + +View Zoom Team Chat Group information + +**Associated APIs:** + +- [List IM directory group members](/docs/api/rest/reference/chat/methods/#operation/imGroupMembers) +- [List IM directory groups](/docs/api/rest/reference/chat/methods/#operation/imGroups) +- [Retrieve an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroup) + +### imgroup:write:admin + +View and manage Zoom Team Chat Groups + +**Associated APIs:** + +- [Delete an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupDelete) +- [Create an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupCreate) +- [Update an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupUpdate) +- [List IM directory group members](/docs/api/rest/reference/chat/methods/#operation/imGroupMembers) +- [List IM directory groups](/docs/api/rest/reference/chat/methods/#operation/imGroups) +- [Retrieve an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroup) +- [Add IM directory group members](/docs/api/rest/reference/chat/methods/#operation/imGroupMembersCreate) +- [Delete IM directory group member](/docs/api/rest/reference/chat/methods/#operation/imGroupMembersDelete) + +## Integration Healthcare + +### clinical_note:read:admin + +Read Clinical Notes + +### clinical_note:update:admin + +Update Clinical Notes + +## Mail + +### mail:read + +Read user’s own mailbox content. + +**Associated APIs:** + +- [Get the specified attachment for an email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_attachment) +- [List labels in the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_labels_in_mailbox) +- [Get the specified email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_thread) +- [Get the mailbox profile](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_profile) +- [List email threads from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_threads) +- [Get the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_draft_email) +- [List delegates on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_delegates) +- [Get the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email) +- [List history of events for mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_history) +- [List email filters](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_filters) +- [Get the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_filter) +- [Get the specified delegate on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_delegate) +- [Get mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mail_vacation_response_setting) +- [List emails from draft folder](/docs/api/rest/reference/zoom-mail/methods/#operation/list_draft_emails) +- [Get the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_label_in_mailbox) +- [List emails from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_emails) + +### mail:read:admin + +Read all mailboxes' content under the account, and account level settings. + +**Associated APIs:** + +- [Get the mailbox profile](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_profile) +- [Get the specified attachment for an email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_attachment) +- [Get the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_filter) +- [Get the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email) +- [Get the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_draft_email) +- [Get mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mail_vacation_response_setting) +- [Get the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_label_in_mailbox) +- [List emails from draft folder](/docs/api/rest/reference/zoom-mail/methods/#operation/list_draft_emails) +- [Get the specified delegate on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_delegate) +- [List emails from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_emails) +- [List labels in the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_labels_in_mailbox) +- [Get the specified email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_thread) +- [List history of events for mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_history) +- [List email filters](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_filters) +- [List email threads from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_threads) +- [List delegates on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_delegates) + +### mail:write + +Read and write user’s own mailbox content. + +**Associated APIs:** + +- [Delete an existing email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_thread) +- [Create a new draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_draft_email) +- [Create a new email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email) +- [Delete an existing label from mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_label_from_mailbox) +- [Grant a new delegate access on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/grant_mailbox_delegate) +- [Send out a draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_draft_email) +- [Update mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/update_mailbox_vacation_response_setting) +- [Batch modify the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_modify_emails) +- [Delete an existing draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_draft_email) +- [Revoke an existing delegate access from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/revoke_mailbox_delegate) +- [Update the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_draft_email) +- [Create a new label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/create_label_in_mailbox) +- [Patch the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/patch_label_in_mailbox) +- [Send out an email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_email) +- [Move the specified thread out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email_thread) +- [Delete an existing email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email) +- [Move the specified thread to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email_thread) +- [Update the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/update_label_in_mailbox) +- [Update the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email) +- [Update the specified thread](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email_thread) +- [Batch delete the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_delete_emails) +- [Move the specified email to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email) +- [Move the specified email out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email) +- [Create an email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email_filter) +- [Delete the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_filter) + +### mail:write:admin + +Read and write all mailboxes' content under the account, and account level settings. + +**Associated APIs:** + +- [Update mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/update_mailbox_vacation_response_setting) +- [Delete an existing label from mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_label_from_mailbox) +- [Move the specified thread to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email_thread) +- [Update the specified thread](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email_thread) +- [Revoke an existing delegate access from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/revoke_mailbox_delegate) +- [Move the specified email out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email) +- [Delete an existing email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_thread) +- [Batch modify the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_modify_emails) +- [Create a new draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_draft_email) +- [Update the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_draft_email) +- [Patch the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/patch_label_in_mailbox) +- [Move the specified email to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email) +- [Update the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/update_label_in_mailbox) +- [Delete an existing draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_draft_email) +- [Send out a draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_draft_email) +- [Update the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email) +- [Send out an email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_email) +- [Create a new email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email) +- [Create an email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email_filter) +- [Move the specified thread out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email_thread) +- [Delete an existing email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email) +- [Delete the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_filter) +- [Create a new label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/create_label_in_mailbox) +- [Grant a new delegate access on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/grant_mailbox_delegate) +- [Batch delete the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_delete_emails) + +## Marketplace + +### marketplace_app:master + +View and manage sub accounts' marketplace app info + +**Associated APIs:** + +- [Create apps](/docs/api/rest/reference/marketplace/ma/#operation/CreateApps) + +### marketplace_app:read + +View your marketplace app information + +**Associated APIs:** + +- [Get information about an app](/docs/api/rest/reference/marketplace/methods/#operation/getAppInfo) +- [Get a user's app requests](/docs/api/rest/reference/marketplace/methods/#operation/getUserAppRequests) +- [Validate an app manifest](/docs/api/rest/reference/marketplace/methods/#operation/validatingManifest) +- [Export an app manifest from an existing app](/docs/api/rest/reference/marketplace/methods/#operation/getAppManifest) +- [Get webhook logs](/docs/api/rest/reference/marketplace/methods/#operation/getWebhookLogs) + +### marketplace_app:read:admin + +View marketplace app information for the account + +**Associated APIs:** + +- [Export an app manifest from an existing app](/docs/api/rest/reference/marketplace/methods/#operation/getAppManifest) +- [Get a user's app requests](/docs/api/rest/reference/marketplace/methods/#operation/getUserAppRequests) +- [Get an app's user requests](/docs/api/rest/reference/marketplace/methods/#operation/getAppUserRequests) +- [Get webhook logs](/docs/api/rest/reference/marketplace/methods/#operation/getWebhookLogs) +- [Validate an app manifest](/docs/api/rest/reference/marketplace/methods/#operation/validatingManifest) +- [Get information about an app](/docs/api/rest/reference/marketplace/methods/#operation/getAppInfo) +- [List apps](/docs/api/rest/reference/marketplace/methods/#operation/ListApps) + +### marketplace_app:write + +Edit marketplace app + +**Associated APIs:** + +- [Deletes an app](/docs/api/rest/reference/marketplace/methods/#operation/deleteApp) +- [Create apps](/docs/api/rest/reference/marketplace/methods/#operation/CreateApps) +- [Update an app by manifest](/docs/api/rest/reference/marketplace/methods/#operation/updateAppByManifest) + +### marketplace_app:write:admin + +View and Manage marketplace app information for the account + +**Associated APIs:** + +- [Enable or disable user app subscription](/docs/api/rest/reference/marketplace/methods/#operation/Enable/Disableuserappsubscription) +- [Update app pre approval setting](/docs/api/rest/reference/marketplace/methods/#operation/Updateapppreapprovalsetting) +- [Add app allow requests for users](/docs/api/rest/reference/marketplace/methods/#operation/AddAppAllowRequestsForUsers) +- [Generate Zoom App Deeplink](/docs/api/rest/reference/marketplace/methods/#operation/GenerateZoomAppDeeplink) +- [Update an app by manifest](/docs/api/rest/reference/marketplace/methods/#operation/updateAppByManifest) +- [Create apps](/docs/api/rest/reference/marketplace/methods/#operation/CreateApps) +- [Update app's request status](/docs/api/rest/reference/marketplace/methods/#operation/updateAppRequestStatus) +- [Deletes an app](/docs/api/rest/reference/marketplace/methods/#operation/deleteApp) + +### marketplace_custom_fields:read + +Read custom field values + +**Associated APIs:** + +- [Get user's custom field values](/docs/api/rest/reference/marketplace/methods/#operation/getCustomFieldValues) + +### marketplace_custom_fields:read:admin + +Read custom field values + +**Associated APIs:** + +- [Get user's custom field values](/docs/api/rest/reference/marketplace/methods/#operation/getCustomFieldValues) + +### marketplace_entitlement:read + +View entitlements + +**Associated APIs:** + +- [Get app user entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getAppUserEntitlementRequests) +- [Get a user's entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getUserEntitlementRequests) + +### marketplace_entitlement:read:admin + +View entitlements + +**Associated APIs:** + +- [Get app user entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getAppUserEntitlementRequests) +- [Get a user's entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getUserEntitlementRequests) + +## Meeting + +### information_barriers:read:admin + +View information barriers + +**Associated APIs:** + +- [List information Barrier policies](/docs/api/rest/reference/account/methods/#operation/InformationBarriersList) +- [Get an Information Barrier policy by ID](/docs/api/rest/reference/account/methods/#operation/InformationBarriersGet) + +### information_barriers:read:master + +View information barriers + +**Associated APIs:** + +- [Get an Information Barrier policy by ID](/docs/api/rest/reference/account/ma/#operation/InformationBarriersGet) +- [List information Barrier policies](/docs/api/rest/reference/account/ma/#operation/InformationBarriersList) + +### information_barriers:write:admin + +View and manage information barriers + +**Associated APIs:** + +- [Remove an Information Barrier policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersDelete) +- [Create an Information Barrier policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersCreate) +- [Update an Information Barriers policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersUpdate) + +### information_barriers:write:master + +View and manage information barriers + +**Associated APIs:** + +- [Remove an Information Barrier policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersDelete) +- [Create an Information Barrier policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersCreate) +- [Update an Information Barriers policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersUpdate) + +### meeting:master + +View and manage sub account's user meetings + +**Associated APIs:** + +- [List meeting registrants](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrants) +- [List meeting templates](/docs/api/rest/reference/zoom-api/ma/#operation/listMeetingTemplates) +- [Update livestream status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingLiveStreamStatusUpdate) +- [List registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantsQuestionsGet) +- [Add a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantCreate) +- [Create a meeting template from an existing meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingTemplateCreate) +- [List past meeting instances](/docs/api/rest/reference/zoom-api/ma/#operation/pastMeetings) +- [List meetings](/docs/api/rest/reference/zoom-api/ma/#operation/meetings) +- [Use in-meeting controls](/docs/api/rest/reference/zoom-api/ma/#operation/inMeetingControl) +- [Create a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingCreate) +- [Get a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyGet) +- [Update a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingUpdate) +- [Update a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyUpdate) +- [Delete a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingregistrantdelete) +- [List meeting polls](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPolls) +- [Create a meeting's invite links](/docs/api/rest/reference/zoom-api/ma/#operation/meetingInviteLinksCreate) +- [Get livestream details](/docs/api/rest/reference/zoom-api/ma/#operation/getMeetingLiveStreamDetails) +- [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantQuestionUpdate) +- [Get a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantGet) +- [Update registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantStatus) +- [Get meeting invitation](/docs/api/rest/reference/zoom-api/ma/#operation/meetingInvitation) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/ma/#operation/addBatchRegistrants) +- [Update a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollUpdate) +- [Get a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meeting) +- [Get a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollGet) +- [Delete a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyDelete) +- [Delete a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollDelete) +- [Delete a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingDelete) +- [Update meeting status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingStatus) +- [Update a livestream](/docs/api/rest/reference/zoom-api/ma/#operation/meetingLiveStreamUpdate) +- [Get meeting's token](/docs/api/rest/reference/zoom-api/ma/#operation/meetingToken) +- [Create a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollCreate) +- [Perform batch poll creation](/docs/api/rest/reference/zoom-api/ma/#operation/createBatchPolls) + +### meeting:read + +View your meetings + +**Associated APIs:** + +- [Get livestream details](/docs/api/rest/reference/zoom-api/methods/#operation/getMeetingLiveStreamDetails) +- [List past meeting instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetings) +- [Get a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyGet) +- [Get a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantGet) +- [List meetings](/docs/api/rest/reference/zoom-api/methods/#operation/meetings) +- [Get a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meeting) +- [List past meeting's poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingPolls) +- [Get past meeting details](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingDetails) +- [List past meetings' Q&A](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingQA) +- [List meeting templates](/docs/api/rest/reference/zoom-api/methods/#operation/listMeetingTemplates) +- [Get past meeting participants](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingParticipants) +- [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantsQuestionsGet) +- [Get meeting invitation](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInvitation) +- [List meeting registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrants) +- [Get a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollGet) +- [List meeting polls](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPolls) +- [List upcoming meetings](/docs/api/rest/reference/zoom-api/methods/#operation/listUpcomingMeeting) +- [Get meeting's token](/docs/api/rest/reference/zoom-api/methods/#operation/meetingToken) + +### meeting:read:admin + +View all user meetings + +**Associated APIs:** + +- [Get livestream details](/docs/api/rest/reference/zoom-api/methods/#operation/getMeetingLiveStreamDetails) +- [List past meeting's poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingPolls) +- [List meeting polls](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPolls) +- [Get a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyGet) +- [List past meetings' Q&A](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingQA) +- [List past meeting instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetings) +- [Get past meeting participants](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingParticipants) +- [List meetings](/docs/api/rest/reference/zoom-api/methods/#operation/meetings) +- [Get a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meeting) +- [Get past meeting details](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingDetails) +- [List meeting templates](/docs/api/rest/reference/zoom-api/methods/#operation/listMeetingTemplates) +- [List meeting registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrants) +- [List upcoming meetings](/docs/api/rest/reference/zoom-api/methods/#operation/listUpcomingMeeting) +- [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantsQuestionsGet) +- [Get meeting's token](/docs/api/rest/reference/zoom-api/methods/#operation/meetingToken) +- [Get meeting invitation](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInvitation) +- [Get a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantGet) +- [Get a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollGet) + +### meeting:write + +View and manage your meetings + +**Associated APIs:** + +- [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantStatus) +- [Add a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppAdd) +- [Update livestream status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamStatusUpdate) +- [Add a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantCreate) +- [Delete a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollDelete) +- [Update a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollUpdate) +- [Delete a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyDelete) +- [Delete a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingregistrantdelete) +- [Delete a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteMeetingChatMessageById) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantQuestionUpdate) +- [Create a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollCreate) +- [Perform batch poll creation](/docs/api/rest/reference/zoom-api/methods/#operation/createBatchPolls) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchRegistrants) +- [Create a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate) +- [Delete a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppDelete) +- [Update a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate) +- [Create a meeting's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInviteLinksCreate) +- [Update a livestream](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamUpdate) +- [Update a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyUpdate) +- [Update meeting status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingStatus) +- [Delete a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingDelete) +- [Create a meeting template from an existing meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingTemplateCreate) +- [Use in-meeting controls](/docs/api/rest/reference/zoom-api/methods/#operation/inMeetingControl) +- [Update participant Real-Time Media Streams (RTMS) app status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRTMSStatusUpdate) +- [Update a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/updateMeetingChatMessageById) + +### meeting:write:admin + +View and manage all user meetings + +**Associated APIs:** + +- [Update a livestream](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamUpdate) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchRegistrants) +- [Create a meeting template from an existing meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingTemplateCreate) +- [Delete a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppDelete) +- [Create a meeting's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInviteLinksCreate) +- [Update a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyUpdate) +- [Update meeting status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingStatus) +- [Delete a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteMeetingChatMessageById) +- [Update a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/updateMeetingChatMessageById) +- [Use in-meeting controls](/docs/api/rest/reference/zoom-api/methods/#operation/inMeetingControl) +- [Update participant Real-Time Media Streams (RTMS) app status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRTMSStatusUpdate) +- [Delete a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingDelete) +- [Create a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollCreate) +- [Delete a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyDelete) +- [Add a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppAdd) +- [Update livestream status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamStatusUpdate) +- [Add a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantCreate) +- [Delete a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingregistrantdelete) +- [Delete a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollDelete) +- [Perform batch poll creation](/docs/api/rest/reference/zoom-api/methods/#operation/createBatchPolls) +- [Create a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate) +- [Update a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate) +- [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantStatus) +- [Update a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollUpdate) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantQuestionUpdate) + +### meeting:write:admin:sip_dialing + +Get CRC dial string with passcode + +**Associated APIs:** + +- [Get a meeting SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getSipDialingWithPasscode) + +### meeting:write:sip_dialing + +Get CRC dial string with passcode + +**Associated APIs:** + +- [Get a meeting SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getSipDialingWithPasscode) + +### meeting_summary:master + +View and manage sub account's user meeting summaries + +**Associated APIs:** + +- [Get a meeting or webinar summary](/docs/api/rest/reference/zoom-api/ma/#operation/Getameetingsummary) +- [List an account's meeting or webinar summaries](/docs/api/rest/reference/zoom-api/ma/#operation/Listmeetingsummaries) +- [Delete a meeting or webinar summary](/docs/api/rest/reference/zoom-api/ma/#operation/Deletemeetingorwebinarsummary) + +### meeting_summary:read + +View your meeting summaries + +**Associated APIs:** + +- [Get a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Getameetingsummary) + +### meeting_summary:read:admin + +View all user meeting summaries + +**Associated APIs:** + +- [List an account's meeting or webinar summaries](/docs/api/rest/reference/zoom-api/methods/#operation/Listmeetingsummaries) +- [Get a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Getameetingsummary) + +### meeting_token:read:admin:live_streaming + +View live streaming meeting token information + +**Associated APIs:** + +- [Get a meeting's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamingJoinToken) + +### meeting_token:read:admin:local_archiving + +View local archiving meeting token information + +**Associated APIs:** + +- [Get a meeting's archive token for local archiving](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLocalArchivingArchiveToken) + +### meeting_token:read:admin:local_recording + +This scope allows an app to view an account's users' local recording meeting token information + +**Associated APIs:** + +- [Get a meeting's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLocalRecordingJoinToken) + +### meeting_token:read:live_streaming + +View live streaming meeting token information + +**Associated APIs:** + +- [Get a meeting's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamingJoinToken) + +### meeting_token:read:local_recording + +This scope allows an app to view a user's local recording meeting token information + +**Associated APIs:** + +- [Get a meeting's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLocalRecordingJoinToken) + +## NumberManagement + +### number_management_numbers:read:admin + +View all phone number detail in "Number Management". + +### number_management_numbers:read:master + +View and manage sub account's phone number detail in "Number Management". + +### number_management_numbers:write:admin + +Manage phone number detail in "Number Management". + +### number_management_numbers:write:master + +Manage sub account's phone number detail in "Number Management". + +## PAC + +### pac:master + +**Associated APIs:** + +- [List a user's PAC accounts](/docs/api/rest/reference/zoom-api/ma/#operation/userPACs) + +### pac:read + +View your audio conference info + +**Associated APIs:** + +- [List a user's PAC accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userPACs) + +### pac:read:admin + +View all users' audio conference info + +**Associated APIs:** + +- [List a user's PAC accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userPACs) + +## Phone + +### phone:master + +View and manage all sub accounts' Zoom Phone information + +**Associated APIs:** + +- [Delete an extension's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/DeleteExtensiontLevelInboundBlockRules) +- [Delete users from a directory](/docs/api/rest/reference/phone/ma/#operation/DeleteUsersFromDirectory) +- [Get device details](/docs/api/rest/reference/phone/ma/#operation/getADevice) +- [Get a list of monitoring groups on an account](/docs/api/rest/reference/phone/ma/#operation/listMonitoringGroup) +- [Upload fax file](/docs/api/rest/reference/phone/ma/#operation/UploadFaxFiles) +- [Update a user's profile](/docs/api/rest/reference/phone/ma/#operation/updateUserProfile) +- [Unassign a member](/docs/api/rest/reference/phone/ma/#operation/unassignMemberFromCallQueue) +- [Add audio items](/docs/api/rest/reference/phone/ma/#operation/AddAudioItem) +- [Update user policy](/docs/api/rest/reference/phone/ma/#operation/updateUserPolicy) +- [Add a Zoom Room to a Zoom Phone](/docs/api/rest/reference/phone/ma/#operation/addZoomRoom) +- [Update user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateUserOutboundCallingExceptionRule) +- [Update an emergency address](/docs/api/rest/reference/phone/ma/#operation/updateEmergencyAddress) +- [Get call log details](/docs/api/rest/reference/phone/ma/#operation/getCallLogDetails) +- [Update an SLG policy setting](/docs/api/rest/reference/phone/ma/#operation/updateSLGPolicySubSetting) +- [List call logs](/docs/api/rest/reference/phone/ma/#operation/listCallLogsMetrics) +- [Get an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistDetail) +- [Delete a blocked list](/docs/api/rest/reference/phone/ma/#operation/deleteABlockedList) +- [List auto receptionists](/docs/api/rest/reference/phone/ma/#operation/listAutoReceptionists) +- [Assign a phone number to a user](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumber) +- [Download a phone recording transcript](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingTranscript) +- [Add a policy subsetting to a call queue](/docs/api/rest/reference/phone/ma/#operation/addCQPolicySubSetting) +- [Update account level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountOutboundCallingCountriesOrRegions) +- [Add a client code to a call history](/docs/api/rest/reference/phone/ma/#operation/addClientCodeToCallHistory) +- [Assign phone numbers to a common area](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumbersToCommonArea) +- [Unassign a calling plan from the common area](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlansFromCommonArea) +- [Get blocked list details](/docs/api/rest/reference/phone/ma/#operation/getABlockedList) +- [Remove a member from a private directory](/docs/api/rest/reference/phone/ma/#operation/removeAMemberFromAPrivateDirectory) +- [Add phone numbers for users' customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/addUserOutboundCallerNumbers) +- [Delete a call queue](/docs/api/rest/reference/phone/ma/#operation/deleteACallQueue) +- [List real time location for IP phones](/docs/api/rest/reference/phone/ma/#operation/listPhoneRealtimelocation) +- [Delete a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteASharedLineGroup) +- [Create a call queue](/docs/api/rest/reference/phone/ma/#operation/createCallQueue) +- [List opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/getNumberCampaignOptStatus) +- [Update the group call pickup information](/docs/api/rest/reference/phone/ma/#operation/updateGCP) +- [Add members to roles](/docs/api/rest/reference/phone/ma/#operation/AddRoleMembers) +- [Get common area settings](/docs/api/rest/reference/phone/ma/#operation/getCommonAreaSettings) +- [Sync user's call history](/docs/api/rest/reference/phone/ma/#operation/syncUserCallHistory) +- [List setting templates](/docs/api/rest/reference/phone/ma/#operation/listSettingTemplates) +- [Get call QoS](/docs/api/rest/reference/phone/ma/#operation/getCallQoS) +- [List customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/listSiteCustomizeOutboundCallerNumbers) +- [List users' phone numbers for a customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/listUserCustomizeOutboundCallerNumbers) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetailsByCallIdOrCallLogId) +- [Get group policy details](/docs/api/rest/reference/phone/ma/#operation/GetGroupPolicyDetails) +- [Delete an audio item](/docs/api/rest/reference/phone/ma/#operation/DeleteAudioItem) +- [Get user level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetUserOutboundCallingCountriesAndRegions) +- [Update an alert setting](/docs/api/rest/reference/phone/ma/#operation/UpdateAnAlertSetting) +- [Delete a device](/docs/api/rest/reference/phone/ma/#operation/deleteADevice) +- [Create a blocked list](/docs/api/rest/reference/phone/ma/#operation/addAnumberToBlockedList) +- [Assign an entity to a device](/docs/api/rest/reference/phone/ma/#operation/addExtensionsToADevice) +- [Update Recording Status](/docs/api/rest/reference/phone/ma/#operation/UpdateRecordingStatus) +- [Update a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/updateUserSetting) +- [Remove a Zoom Room from a ZP account](/docs/api/rest/reference/phone/ma/#operation/RemoveZoomRoom) +- [Get account's call logs](/docs/api/rest/reference/phone/ma/#operation/accountCallLogs) +- [Delete an account's inbound blocked statistics](/docs/api/rest/reference/phone/ma/#operation/DeleteAccountLevelInboundBlockedStatistics) +- [Get fax charges usage report](/docs/api/rest/reference/phone/ma/#operation/Getfaxchargesusagereport) +- [List phone users](/docs/api/rest/reference/phone/ma/#operation/listPhoneUsers) +- [Update Voicemail Read Status](/docs/api/rest/reference/phone/ma/#operation/updateVoicemailReadStatus) +- [Update a shared line group policy](/docs/api/rest/reference/phone/ma/#operation/updateSharedLineGroupPolicy) +- [Update a provision template](/docs/api/rest/reference/phone/ma/#operation/updateProvisionTemplate) +- [Delete common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteCommonAreaOutboundCallingExceptionRule) +- [Delete directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/deleteRoutingRule) +- [Assign numbers to a call queue](/docs/api/rest/reference/phone/ma/#operation/assignPhoneToCallQueue) +- [List call queues](/docs/api/rest/reference/phone/ma/#operation/listCallQueues) +- [Add a site setting](/docs/api/rest/reference/phone/ma/#operation/addSiteSetting) +- [Add a setting template](/docs/api/rest/reference/phone/ma/#operation/addSettingTemplate) +- [Get emergency address details](/docs/api/rest/reference/phone/ma/#operation/getEmergencyAddress) +- [List audio items](/docs/api/rest/reference/phone/ma/#operation/ListAudioItems) +- [Update an audio item](/docs/api/rest/reference/phone/ma/#operation/UpdateAudioItem) +- [Get call queue recordings](/docs/api/rest/reference/phone/ma/#operation/getCallQueueRecordings) +- [List Zoom Rooms without Zoom Phone assignment](/docs/api/rest/reference/phone/ma/#operation/listUnassignedZoomRooms) +- [Add peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/addPeeringPhoneNumbers) +- [Apply template to common areas](/docs/api/rest/reference/phone/ma/#operation/ApplyTemplatetoCommonAreas) +- [Update phone account settings](/docs/api/rest/reference/phone/ma/#operation/updatePhoneSettings) +- [Generate activation codes for common areas](/docs/api/rest/reference/phone/ma/#operation/Generateactivationcodesforcommonareas) +- [Update a shared line group](/docs/api/rest/reference/phone/ma/#operation/updateASharedLineGroup) +- [Delete an emergency location](/docs/api/rest/reference/phone/ma/#operation/deleteLocation) +- [List an extension's inbound block rules](/docs/api/rest/reference/phone/ma/#operation/ListExtensionLevelInboundBlockRules) +- [Remove a calling plan from a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlanFromRoom) +- [Get phone account settings](/docs/api/rest/reference/phone/ma/#operation/phoneSetting) +- [List plan information](/docs/api/rest/reference/phone/ma/#operation/listPhonePlans) +- [Remove members from call pickup group](/docs/api/rest/reference/phone/ma/#operation/removeGCPMembers) +- [Update common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaOutboundCallingExceptionRule) +- [Get a phone number](/docs/api/rest/reference/phone/ma/#operation/getPhoneNumberDetails) +- [Add BYOC phone numbers](/docs/api/rest/reference/phone/ma/#operation/addBYOCNumber) +- [Update account policy](/docs/api/rest/reference/phone/ma/#operation/updateAccountPolicy) +- [Update Auto Delete Field](/docs/api/rest/reference/phone/ma/#operation/UpdateAutoDeleteField) +- [List nomadic emergency services users](/docs/api/rest/reference/phone/ma/#operation/listUserNomadicEmergencyServices) +- [Get alert setting details](/docs/api/rest/reference/phone/ma/#operation/GetAlertSettingDetails) +- [Add members to a monitoring group](/docs/api/rest/reference/phone/ma/#operation/addMembers) +- [Add an external contact](/docs/api/rest/reference/phone/ma/#operation/addExternalContact) +- [Delete a user's call log](/docs/api/rest/reference/phone/ma/#operation/deleteCallLog) +- [Unassign all phone numbers](/docs/api/rest/reference/phone/ma/#operation/unassignAllPhoneNumsAutoReceptionist) +- [Get a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/getZoomRoom) +- [Delete site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteSiteOutboundCallingExceptionRule) +- [The list of shared line groups](/docs/api/rest/reference/phone/ma/#operation/listSharedLineGroups) +- [Delete an emergency address](/docs/api/rest/reference/phone/ma/#operation/deleteEmergencyAddress) +- [List activation codes](/docs/api/rest/reference/phone/ma/#operation/listActivationCodes) +- [Batch add emergency service locations](/docs/api/rest/reference/phone/ma/#operation/batchAddLocations) +- [List phone numbers](/docs/api/rest/reference/phone/ma/#operation/listCRPhoneNumbers) +- [Send fax](/docs/api/rest/reference/phone/ma/#operation/SendEFax) +- [List emergency addresses](/docs/api/rest/reference/phone/ma/#operation/listEmergencyAddresses) +- [List firmware update rules](/docs/api/rest/reference/phone/ma/#operation/ListFirmwareRules) +- [Get call recordings](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordings) +- [Update a phone number](/docs/api/rest/reference/phone/ma/#operation/updatePhoneNumberDetails) +- [Update user level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateUserOutboundCallingCountriesOrRegions) +- [Update a device](/docs/api/rest/reference/phone/ma/#operation/updateADevice) +- [Unassign a member from a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteAMemberSLG) +- [Update an auto receptionist policy](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionistPolicy) +- [Sync user's call logs](/docs/api/rest/reference/phone/ma/#operation/syncUserCallLogs) +- [Add an emergency address](/docs/api/rest/reference/phone/ma/#operation/addEmergencyAddress) +- [Add a policy setting to a shared line group](/docs/api/rest/reference/phone/ma/#operation/addSLGPolicySubSetting) +- [Get account policy details](/docs/api/rest/reference/phone/ma/#operation/GetAccountPolicyDetails) +- [List common areas](/docs/api/rest/reference/phone/ma/#operation/listCommonAreas) +- [Delete an external contact](/docs/api/rest/reference/phone/ma/#operation/deleteAExternalContact) +- [List ported numbers](/docs/api/rest/reference/phone/ma/#operation/listPortedNumbers) +- [Unassign members from a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteMembersOfSLG) +- [Get a provision template](/docs/api/rest/reference/phone/ma/#operation/GetProvisionTemplate) +- [Get user's recordings](/docs/api/rest/reference/phone/ma/#operation/phoneUserRecordings) +- [Delete a monitoring group](/docs/api/rest/reference/phone/ma/#operation/deleteMonitoringGroup) +- [Update a phone role](/docs/api/rest/reference/phone/ma/#operation/UpdatePhoneRole) +- [Delete a common area](/docs/api/rest/reference/phone/ma/#operation/deleteCommonArea) +- [Sync SMS by session ID](/docs/api/rest/reference/phone/ma/#operation/smsSessionSync) +- [Add common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddCommonAreaOutboundCallingExceptionRule) +- [Get members of a monitoring group](/docs/api/rest/reference/phone/ma/#operation/listMembers) +- [Get call details from call log](/docs/api/rest/reference/phone/ma/#operation/getCallLogMetricsDetails) +- [Add members to a call queue](/docs/api/rest/reference/phone/ma/#operation/addMembersToCallQueue) +- [List blocked lists](/docs/api/rest/reference/phone/ma/#operation/listBlockedList) +- [List tracked locations](/docs/api/rest/reference/phone/ma/#operation/listTrackedLocations) +- [Update common area pin code](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaPinCode) +- [Get a phone site setting](/docs/api/rest/reference/phone/ma/#operation/getSiteSettingForType) +- [Add phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/addOutboundCallerNumbers) +- [List BYOC SIP trunks](/docs/api/rest/reference/phone/ma/#operation/listBYOCSIPTrunk) +- [Get SMS session details](/docs/api/rest/reference/phone/ma/#operation/smsSessionDetails) +- [Get call queue details](/docs/api/rest/reference/phone/ma/#operation/getACallQueue) +- [List users in directory](/docs/api/rest/reference/phone/ma/#operation/ListUsersFromDirectory) +- [Reboot a desk phone](/docs/api/rest/reference/phone/ma/#operation/rebootPhoneDevice) +- [Update opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/updateNumberCampaignOptStatus) +- [Get monitoring group by ID](/docs/api/rest/reference/phone/ma/#operation/getMonitoringGroupById) +- [Get user's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/userSmsSession) +- [Get SMS/MMS charges usage report](/docs/api/rest/reference/phone/ma/#operation/GetSMSChargesUsageReport) +- [Batch update device line key position](/docs/api/rest/reference/phone/ma/#operation/batchUpdateDeviceLineKeySetting) +- [Delete a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/deleteUserSetting) +- [Unassign an entity from the device](/docs/api/rest/reference/phone/ma/#operation/deleteExtensionFromADevice) +- [Assign phone numbers](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumbersAutoReceptionist) +- [Update emergency service location](/docs/api/rest/reference/phone/ma/#operation/updateLocation) +- [Assign a phone number to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/assignCampaignPhoneNumbers) +- [List an account's inbound blocked statistics](/docs/api/rest/reference/phone/ma/#operation/ListAccountLevelInboundBlockedStatistics) +- [Update common area](/docs/api/rest/reference/phone/ma/#operation/updateCommonArea) +- [Download a phone voicemail](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadVoicemailFile) +- [Delete firmware update rule](/docs/api/rest/reference/phone/ma/#operation/DeleteFirmwareUpdateRule) +- [Get account's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/accountSmsSession) +- [Download fax file](/docs/api/rest/reference/phone/ma/#operation/Downloadfaxfile) +- [Add directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/addRoutingRule) +- [Get a user's profile settings](/docs/api/rest/reference/phone/ma/#operation/phoneUserSettings) +- [Get ported numbers details](/docs/api/rest/reference/phone/ma/#operation/getPortedNumbersDetails) +- [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/unAssignPhoneNumCallQueue) +- [Delete a call recording](/docs/api/rest/reference/phone/ma/#operation/deleteCallRecording) +- [Get account voicemails](/docs/api/rest/reference/phone/ma/#operation/accountVoiceMails) +- [Add a device](/docs/api/rest/reference/phone/ma/#operation/addPhoneDevice) +- [List real time location for users](/docs/api/rest/reference/phone/ma/#operation/listUserRealtimeLocation) +- [Add an audio item for text-to-speech conversion](/docs/api/rest/reference/phone/ma/#operation/AddAnAudio) +- [Activate phone numbers](/docs/api/rest/reference/phone/ma/#operation/activeCRPhoneNumbers) +- [Add a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/addUserSetting) +- [Update peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/updatePeeringPhoneNumbers) +- [Add members to a shared line group](/docs/api/rest/reference/phone/ma/#operation/addMembersToSharedLineGroup) +- [Get extension's fax logs](/docs/api/rest/reference/phone/ma/#operation/Getuser'sfaxlogs) +- [Update group policy](/docs/api/rest/reference/phone/ma/#operation/updateGroupPolicy) +- [Get operation logs report](/docs/api/rest/reference/phone/ma/#operation/getPSOperationLogs) +- [List phone sites](/docs/api/rest/reference/phone/ma/#operation/listPhoneSites) +- [Unassign phone numbers from common area](/docs/api/rest/reference/phone/ma/#operation/unassignPhoneNumbersFromCommonArea) +- [Add a client code to a call log](/docs/api/rest/reference/phone/ma/#operation/addClientCodeToCallLog) +- [Get directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/getRoutingRule) +- [Add a call handling setting](/docs/api/rest/reference/phone/ma/#operation/addCallHandling) +- [List phone roles](/docs/api/rest/reference/phone/ma/#operation/ListPhoneRoles) +- [List billing accounts](/docs/api/rest/reference/phone/ma/#operation/listBillingAccount) +- [Get account level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetAccountOutboundCallingCountriesAndRegions) +- [Delete account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteAccountOutboundCallingExceptionRule) +- [Delete phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/deleteOutboundCallerNumbers) +- [Delete a phone role](/docs/api/rest/reference/phone/ma/#operation/DeletePhoneRole) +- [Post SMS message](/docs/api/rest/reference/phone/ma/#operation/postSmsMessage) +- [Delete a voicemail](/docs/api/rest/reference/phone/ma/#operation/deleteVoicemail) +- [Batch add users](/docs/api/rest/reference/phone/ma/#operation/batchAddUsers) +- [Get line key position and settings information](/docs/api/rest/reference/phone/ma/#operation/listLineKeySetting) +- [Sync deskphones](/docs/api/rest/reference/phone/ma/#operation/syncPhoneDevice) +- [Update an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionist) +- [Update user's calling plan](/docs/api/rest/reference/phone/ma/#operation/updateCallingPlan) +- [List account level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listAccountOutboundCallingExceptionRule) +- [Get role information](/docs/api/rest/reference/phone/ma/#operation/getRoleInformation) +- [Update phone site details](/docs/api/rest/reference/phone/ma/#operation/updateSiteDetails) +- [List emergency service locations](/docs/api/rest/reference/phone/ma/#operation/listLocations) +- [List an account's customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/listCustomizeOutboundCallerNumbers) +- [Delete group call pickup objects](/docs/api/rest/reference/phone/ma/#operation/deleteGCP) +- [Get voicemail details](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetails) +- [Get emergency service location details](/docs/api/rest/reference/phone/ma/#operation/getLocation) +- [Add an emergency service location](/docs/api/rest/reference/phone/ma/#operation/addLocation) +- [Update auto receptionist IVR](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionistIVR) +- [Update a user's profile settings](/docs/api/rest/reference/phone/ma/#operation/updateUserSettings) +- [Get auto receptionist IVR](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistIVR) +- [List site level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listSiteOutboundCallingExceptionRule) +- [Update a setting template](/docs/api/rest/reference/phone/ma/#operation/updateSettingTemplate) +- [Get group phone settings](/docs/api/rest/reference/phone/ma/#operation/getGroupPhoneSettings) +- [Add members to a private directory](/docs/api/rest/reference/phone/ma/#operation/addMembersToAPrivateDirectory) +- [List updatable firmwares](/docs/api/rest/reference/phone/ma/#operation/ListFirmwares) +- [Add a group call pickup object](/docs/api/rest/reference/phone/ma/#operation/addGCP) +- [Download a phone recording](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingFile) +- [Get site level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetSiteOutboundCallingCountriesAndRegions) +- [Mark a phone number as blocked for all extensions](/docs/api/rest/reference/phone/ma/#operation/MarkPhoneNumberAsBlockedForAllExtensions) +- [Delete a call handling setting](/docs/api/rest/reference/phone/ma/#operation/deleteCallHandling) +- [List common area level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listCommonAreaOutboundCallingExceptionRule) +- [Delete a line key setting.](/docs/api/rest/reference/phone/ma/#operation/DeleteLineKey) +- [List opt statuses of phone numbers assigned to SMS consent](/docs/api/rest/reference/phone/ma/#operation/getNumberConsentOptStatus) +- [Create a monitoring group](/docs/api/rest/reference/phone/ma/#operation/createMonitoringGroup) +- [Remove all monitors or monitored members from a monitoring group](/docs/api/rest/reference/phone/ma/#operation/removeMembers) +- [Get setting template details](/docs/api/rest/reference/phone/ma/#operation/getSettingTemplate) +- [Delete a non-primary auto receptionist](/docs/api/rest/reference/phone/ma/#operation/deleteAutoReceptionist) +- [Update a call handling setting](/docs/api/rest/reference/phone/ma/#operation/updateCallHandling) +- [Create a phone site](/docs/api/rest/reference/phone/ma/#operation/createPhoneSite) +- [Update common area level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaOutboundCallingCountriesOrRegions) +- [Get User AI Call Summary Detail](/docs/api/rest/reference/phone/ma/#operation/getUserAICallSummary) +- [Get common area details](/docs/api/rest/reference/phone/ma/#operation/getACommonArea) +- [List directory backup routing rules](/docs/api/rest/reference/phone/ma/#operation/listRoutingRule) +- [Update the site setting](/docs/api/rest/reference/phone/ma/#operation/updateSiteSetting) +- [Delete a site setting](/docs/api/rest/reference/phone/ma/#operation/deleteSiteSetting) +- [Delete unassigned phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteUnassignedPhoneNumbers) +- [Delete a phone site](/docs/api/rest/reference/phone/ma/#operation/deletePhoneSite) +- [List an account's Zoom Phone settings](/docs/api/rest/reference/phone/ma/#operation/listZoomPhoneAccountSettings) +- [Get fax log details](/docs/api/rest/reference/phone/ma/#operation/GetFaxLogDetails) +- [Get a shared line group policy](/docs/api/rest/reference/phone/ma/#operation/getSharedLineGroupPolicy) +- [Add customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/addSiteOutboundCallerNumbers) +- [Get a user's profile](/docs/api/rest/reference/phone/ma/#operation/phoneUser) +- [Get call pickup group by ID](/docs/api/rest/reference/phone/ma/#operation/GetGCP) +- [List call queue members](/docs/api/rest/reference/phone/ma/#operation/listCallQueueMembers) +- [List call pickup group members](/docs/api/rest/reference/phone/ma/#operation/listGCPMembers) +- [Duplicate a phone role](/docs/api/rest/reference/phone/ma/#operation/DuplicatePhoneRole) +- [Update a blocked list](/docs/api/rest/reference/phone/ma/#operation/updateBlockedList) +- [List members in a role](/docs/api/rest/reference/phone/ma/#operation/ListRoleMembers) +- [List group call pickup objects](/docs/api/rest/reference/phone/ma/#operation/listGCP) +- [Add a provision template](/docs/api/rest/reference/phone/ma/#operation/addProvisionTemplate) +- [Update a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/updatePolicy) +- [List carrier peering phone numbers.](/docs/api/rest/reference/phone/ma/#operation/listCarrierPeeringPhoneNumbers) +- [Assign calling plan to a user](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlan) +- [Remove users' customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteUserOutboundCallerNumbers) +- [Delete a user's call history](/docs/api/rest/reference/phone/ma/#operation/deleteUserCallHistory) +- [List SMS campaigns](/docs/api/rest/reference/phone/ma/#operation/listAccountSMSCampaigns) +- [Delete common area setting](/docs/api/rest/reference/phone/ma/#operation/deleteCommonAreaSetting) +- [Add members to a call pickup group](/docs/api/rest/reference/phone/ma/#operation/addGCPMembers) +- [List devices](/docs/api/rest/reference/phone/ma/#operation/listPhoneDevices) +- [Update a monitoring group](/docs/api/rest/reference/phone/ma/#operation/updateMonitoringGroup) +- [List private directory members](/docs/api/rest/reference/phone/ma/#operation/listPrivateDirectoryMembers) +- [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/ma/#operation/GetSmsSessions) +- [Remove a phone number from a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/UnassignPhoneNumberFromZoomRoom) +- [List shared line appearances](/docs/api/rest/reference/phone/ma/#operation/listSharedLineAppearances) +- [Add users to a directory](/docs/api/rest/reference/phone/ma/#operation/AddUsersToDirectory) +- [Remove customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteSiteOutboundCallerNumbers) +- [Delete a phone number](/docs/api/rest/reference/phone/ma/#operation/deleteCRPhoneNumber) +- [Add an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/AddAccountLevelInboundBlockRules) +- [Add common area setting](/docs/api/rest/reference/phone/ma/#operation/AddCommonAreaSetting) +- [Remove a member from a monitoring group](/docs/api/rest/reference/phone/ma/#operation/removeMember) +- [Delete an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/DeleteAccountLevelInboundBlockRules) +- [List alert settings with paging query](/docs/api/rest/reference/phone/ma/#operation/ListAlertSettingsWithPagingQuery) +- [Get external contact details](/docs/api/rest/reference/phone/ma/#operation/getAExternalContact) +- [Update an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountLevelInboundBlockRule) +- [Assign phone numbers to a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumberToZoomRoom) +- [List past call metrics](/docs/api/rest/reference/phone/ma/#operation/listPastCallMetrics) +- [List Smartphones](/docs/api/rest/reference/phone/ma/#operation/ListSmartphones) +- [Update external contact](/docs/api/rest/reference/phone/ma/#operation/updateExternalContact) +- [Add a common area](/docs/api/rest/reference/phone/ma/#operation/addCommonArea) +- [Create a shared line group](/docs/api/rest/reference/phone/ma/#operation/createASharedLineGroup) +- [Get an audio item](/docs/api/rest/reference/phone/ma/#operation/GetAudioItem) +- [List an account's inbound block rules](/docs/api/rest/reference/phone/ma/#operation/ListAccountLevelInboundBlockRules) +- [Add an alert setting](/docs/api/rest/reference/phone/ma/#operation/AddAnAlertSetting) +- [Delete user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteUserOutboundCallingExceptionRule) +- [Update site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateSiteOutboundCallingExceptionRule) +- [Remove peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/deletePeeringPhoneNumbers) +- [Add account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddAccountOutboundCallingExceptionRule) +- [Delete members in a role](/docs/api/rest/reference/phone/ma/#operation/DelRoleMembers) +- [Update a call queue's policy subsetting](/docs/api/rest/reference/phone/ma/#operation/updateCQPolicySubSetting) +- [Update common area setting](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaSetting) +- [List user's opt statuses of phone numbers](/docs/api/rest/reference/phone/ma/#operation/getUserNumberCampaignOptStatus) +- [Update a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/updateZoomRoom) +- [Update multiple users' properties in batch](/docs/api/rest/reference/phone/ma/#operation/updateUsersPropertiesInBatch) +- [Update a private directory member](/docs/api/rest/reference/phone/ma/#operation/updateAPrivateDirectoryMember) +- [Add users to a directory of a site](/docs/api/rest/reference/phone/ma/#operation/AddUsersToDirectoryBySite) +- [Create phone numbers](/docs/api/rest/reference/phone/ma/#operation/createCRPhoneNumbers) +- [Update directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/updateRoutingRule) +- [List SIP groups](/docs/api/rest/reference/phone/ma/#operation/listSipGroups) +- [List users permission for location sharing](/docs/api/rest/reference/phone/ma/#operation/listUserLocationSharingPermission) +- [Get device line keys information](/docs/api/rest/reference/phone/ma/#operation/listDeviceLineKeySetting) +- [Update account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountOutboundCallingExceptionRule) +- [Update a site's unassigned phone numbers](/docs/api/rest/reference/phone/ma/#operation/updateSiteForUnassignedPhoneNumbers) +- [List peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/listPeeringPhoneNumbers) +- [Get call charges usage report](/docs/api/rest/reference/phone/ma/#operation/GetCallChargesUsageReport) +- [Add a firmware update rule](/docs/api/rest/reference/phone/ma/#operation/AddFirmwareRule) +- [Update site level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateSiteOutboundCallingCountriesOrRegions) +- [Get user policy details](/docs/api/rest/reference/phone/ma/#operation/GetUserPolicyDetails) +- [List calling plans](/docs/api/rest/reference/phone/ma/#operation/listCallingPlans) +- [Add an extension's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/AddExtensiontLevelInboundBlockRules) +- [Get firmware update rule information](/docs/api/rest/reference/phone/ma/#operation/GetFirmwareRuleDetail) +- [Batch update line key position and settings information](/docs/api/rest/reference/phone/ma/#operation/BatchUpdateLineKeySetting) +- [Unassign all members](/docs/api/rest/reference/phone/ma/#operation/unassignAllMembers) +- [Get user's call logs](/docs/api/rest/reference/phone/ma/#operation/phoneUserCallLogs) +- [Update provision template of a device](/docs/api/rest/reference/phone/ma/#operation/updateProvisionTemplateToDevice) +- [Get account's fax logs](/docs/api/rest/reference/phone/ma/#operation/GetAccount'sFaxLogs) +- [Delete an alert setting](/docs/api/rest/reference/phone/ma/#operation/DeleteAnAlertSetting) +- [Unassign user's calling plan](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlan) +- [Get SMS by message ID](/docs/api/rest/reference/phone/ma/#operation/smsByMessageId) +- [Delete a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/DeletePolicy) +- [Get recording by call ID](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordingsByCallIdOrCallLogId) +- [List detectable personal location users](/docs/api/rest/reference/phone/ma/#operation/listUserDetectablePersonalLocation) +- [List Zoom Rooms under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/listZoomRooms) +- [Assign calling plans to a common area](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlansToCommonArea) +- [Get common area level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetCommonAreaOutboundCallingCountriesAndRegions) +- [List default emergency address users](/docs/api/rest/reference/phone/ma/#operation/listUserDefaultEmergencyAddress) +- [List provision templates](/docs/api/rest/reference/phone/ma/#operation/listAccountProvisionTemplate) +- [List external contacts](/docs/api/rest/reference/phone/ma/#operation/listExternalContacts) +- [Get an SMS campaign](/docs/api/rest/reference/phone/ma/#operation/GetSMSCampaign) +- [Delete an SLG policy setting](/docs/api/rest/reference/phone/ma/#operation/removeSLGPolicySubSetting) +- [Add an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/addAutoReceptionist) +- [List users in a directory by site](/docs/api/rest/reference/phone/ma/#operation/ListUsersFromDirectoryBySite) +- [Get a shared line group](/docs/api/rest/reference/phone/ma/#operation/getASharedLineGroup) +- [Get phone site details](/docs/api/rest/reference/phone/ma/#operation/getASite) +- [Delete a CQ policy setting](/docs/api/rest/reference/phone/ma/#operation/removeCQPolicySubSetting) +- [Assign calling plans to a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlanToRoom) +- [Get call handling settings](/docs/api/rest/reference/phone/ma/#operation/getCallHandling) +- [Add a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/AddPolicy) +- [Get billing account details](/docs/api/rest/reference/phone/ma/#operation/GetABillingAccount) +- [Delete users from a directory of a site](/docs/api/rest/reference/phone/ma/#operation/DeleteUsersFromDirectoryBySite) +- [List user level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listUserOutboundCallingExceptionRule) +- [Add site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddSiteOutboundCallingExceptionRule) +- [Get user's voicemails](/docs/api/rest/reference/phone/ma/#operation/phoneUserVoiceMails) +- [Update firmware update rule](/docs/api/rest/reference/phone/ma/#operation/UpdateFirmwareRule) +- [Delete a provision template](/docs/api/rest/reference/phone/ma/#operation/deleteProvisionTemplate) +- [Get an auto receptionist policy](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistsPolicy) +- [Add user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddUserOutboundCallingExceptionRule) + +### phone:read + +View your phone information + +**Associated APIs:** + +- [List user's opt statuses of phone numbers](/docs/api/rest/reference/phone/methods/#operation/getUserNumberCampaignOptStatus) +- [Download fax file](/docs/api/rest/reference/phone/methods/#operation/Downloadfaxfile) +- [Download a phone recording transcript](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingTranscript) +- [Get call element](/docs/api/rest/reference/phone/methods/#operation/getCallElement) +- [Get a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/phoneUserSettings) +- [Sync user's call logs](/docs/api/rest/reference/phone/methods/#operation/syncUserCallLogs) +- [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/methods/#operation/GetSmsSessions) +- [List an extension's inbound block rules](/docs/api/rest/reference/phone/methods/#operation/ListExtensionLevelInboundBlockRules) +- [Download a phone recording](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingFile) +- [Get user's recordings](/docs/api/rest/reference/phone/methods/#operation/phoneUserRecordings) +- [Get device line keys information](/docs/api/rest/reference/phone/methods/#operation/listDeviceLineKeySetting) +- [Sync user's call history](/docs/api/rest/reference/phone/methods/#operation/syncUserCallHistory) +- [Get user's voicemails](/docs/api/rest/reference/phone/methods/#operation/phoneUserVoiceMails) +- [Get User AI Call Summary Detail](/docs/api/rest/reference/phone/methods/#operation/getUserAICallSummary) +- [Get extension's fax logs](/docs/api/rest/reference/phone/methods/#operation/Getuser'sfaxlogs) +- [Get call history detail](/docs/api/rest/reference/phone/methods/#operation/getCallHistoryDetail) +- [List users' phone numbers for a customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/listUserCustomizeOutboundCallerNumbers) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetailsByCallIdOrCallLogId) +- [List audio items](/docs/api/rest/reference/phone/methods/#operation/ListAudioItems) +- [Get user's call logs](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallLogs) +- [Sync SMS by session ID](/docs/api/rest/reference/phone/methods/#operation/smsSessionSync) +- [Get SMS session details](/docs/api/rest/reference/phone/methods/#operation/smsSessionDetails) +- [Get voicemail details](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetails) +- [Get a user's profile](/docs/api/rest/reference/phone/methods/#operation/phoneUser) +- [Get call log details](/docs/api/rest/reference/phone/methods/#operation/getCallLogDetails) +- [Get line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/listLineKeySetting) +- [Get an audio item](/docs/api/rest/reference/phone/methods/#operation/GetAudioItem) +- [Get user's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/userSmsSession) +- [Get SMS by message ID](/docs/api/rest/reference/phone/methods/#operation/smsByMessageId) +- [Download a phone voicemail](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadVoicemailFile) + +### phone:read:admin + +View all users' phone information + +**Associated APIs:** + +- [Get directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/getRoutingRule) +- [List emergency addresses](/docs/api/rest/reference/phone/methods/#operation/listEmergencyAddresses) +- [Get call history detail](/docs/api/rest/reference/phone/ma/#operation/getCallHistoryDetail) +- [Get a phone number](/docs/api/rest/reference/phone/methods/#operation/getPhoneNumberDetails) +- [Get a provision template](/docs/api/rest/reference/phone/methods/#operation/GetProvisionTemplate) +- [Get call charges usage report](/docs/api/rest/reference/phone/methods/#operation/GetCallChargesUsageReport) +- [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/methods/#operation/GetSmsSessions) +- [Get an audio item](/docs/api/rest/reference/phone/methods/#operation/GetAudioItem) +- [List alert settings with paging query](/docs/api/rest/reference/phone/methods/#operation/ListAlertSettingsWithPagingQuery) +- [List phone numbers](/docs/api/rest/reference/phone/methods/#operation/listCRPhoneNumbers) +- [List an account's inbound block rules](/docs/api/rest/reference/phone/methods/#operation/ListAccountLevelInboundBlockRules) +- [List phone role targets](/docs/api/rest/reference/phone/methods/#operation/ListPhoneRoleTargets) +- [List ported numbers](/docs/api/rest/reference/phone/methods/#operation/listPortedNumbers) +- [Get external contact details](/docs/api/rest/reference/phone/methods/#operation/getAExternalContact) +- [List firmware update rules](/docs/api/rest/reference/phone/methods/#operation/ListFirmwareRules) +- [List default emergency address users](/docs/api/rest/reference/phone/methods/#operation/listUserDefaultEmergencyAddress) +- [Get device line keys information](/docs/api/rest/reference/phone/methods/#operation/listDeviceLineKeySetting) +- [Get SMS by message ID](/docs/api/rest/reference/phone/methods/#operation/smsByMessageId) +- [List common areas](/docs/api/rest/reference/phone/methods/#operation/listCommonAreas) +- [Get user's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/userSmsSession) +- [Get emergency address details](/docs/api/rest/reference/phone/methods/#operation/getEmergencyAddress) +- [List BYOC SIP trunks](/docs/api/rest/reference/phone/methods/#operation/listBYOCSIPTrunk) +- [Get call recordings](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordings) +- [List users permission for location sharing](/docs/api/rest/reference/phone/methods/#operation/listUserLocationSharingPermission) +- [List past call metrics](/docs/api/rest/reference/phone/methods/#operation/listPastCallMetrics) +- [Get account policy details](/docs/api/rest/reference/phone/methods/#operation/GetAccountPolicyDetails) +- [Get a shared line group policy](/docs/api/rest/reference/phone/methods/#operation/getSharedLineGroupPolicy) +- [Get user's call logs](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallLogs) +- [Get a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/getZoomRoom) +- [The list of shared line groups](/docs/api/rest/reference/phone/methods/#operation/listSharedLineGroups) +- [List user's opt statuses of phone numbers](/docs/api/rest/reference/phone/methods/#operation/getUserNumberCampaignOptStatus) +- [Sync user's call logs](/docs/api/rest/reference/phone/methods/#operation/syncUserCallLogs) +- [List calling plans](/docs/api/rest/reference/phone/methods/#operation/listCallingPlans) +- [List blocked lists](/docs/api/rest/reference/phone/methods/#operation/listBlockedList) +- [Get call details from call log](/docs/api/rest/reference/phone/methods/#operation/getCallLogMetricsDetails) +- [Get firmware update rule information](/docs/api/rest/reference/phone/methods/#operation/GetFirmwareRuleDetail) +- [Get members of a monitoring group](/docs/api/rest/reference/phone/methods/#operation/listMembers) +- [List users in directory](/docs/api/rest/reference/phone/methods/#operation/ListUsersFromDirectory) +- [List peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/listPeeringPhoneNumbers) +- [Get SMS/MMS charges usage report](/docs/api/rest/reference/phone/methods/#operation/GetSMSChargesUsageReport) +- [Get user's recordings](/docs/api/rest/reference/phone/methods/#operation/phoneUserRecordings) +- [List detectable personal location users](/docs/api/rest/reference/phone/methods/#operation/listUserDetectablePersonalLocation) +- [Get account's fax logs](/docs/api/rest/reference/phone/methods/#operation/GetAccount'sFaxLogs) +- [Get ported numbers details](/docs/api/rest/reference/phone/methods/#operation/getPortedNumbersDetails) +- [Get common area settings](/docs/api/rest/reference/phone/methods/#operation/getCommonAreaSettings) +- [List SIP groups](/docs/api/rest/reference/phone/methods/#operation/listSipGroups) +- [Get recording by call ID](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordingsByCallIdOrCallLogId) +- [Get SMS session details](/docs/api/rest/reference/phone/methods/#operation/smsSessionDetails) +- [Get call queue details](/docs/api/rest/reference/phone/methods/#operation/getACallQueue) +- [Download a phone recording transcript](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingTranscript) +- [Get an SMS campaign](/docs/api/rest/reference/phone/methods/#operation/GetSMSCampaign) +- [List tracked locations](/docs/api/rest/reference/phone/methods/#operation/listTrackedLocations) +- [List provision templates](/docs/api/rest/reference/phone/methods/#operation/listAccountProvisionTemplate) +- [Get common area level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetCommonAreaOutboundCallingCountriesAndRegions) +- [List Zoom Rooms under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/listZoomRooms) +- [List an account's inbound blocked statistics](/docs/api/rest/reference/phone/methods/#operation/ListAccountLevelInboundBlockedStatistics) +- [List external contacts](/docs/api/rest/reference/phone/methods/#operation/listExternalContacts) +- [Get user's voicemails](/docs/api/rest/reference/phone/methods/#operation/phoneUserVoiceMails) +- [Get call history](/docs/api/rest/reference/phone/methods/#operation/getCallPath) +- [Download a phone voicemail](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadVoicemailFile) +- [List an account's customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/listCustomizeOutboundCallerNumbers) +- [Get account voicemails](/docs/api/rest/reference/phone/methods/#operation/accountVoiceMails) +- [Get call handling settings](/docs/api/rest/reference/phone/methods/#operation/getCallHandling) +- [List real time location for users](/docs/api/rest/reference/phone/methods/#operation/listUserRealtimeLocation) +- [Get monitoring group by ID](/docs/api/rest/reference/phone/methods/#operation/getMonitoringGroupById) +- [Get account's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/accountSmsSession) +- [List user level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listUserOutboundCallingExceptionRule) +- [Get an auto receptionist policy](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistsPolicy) +- [Get billing account details](/docs/api/rest/reference/phone/methods/#operation/GetABillingAccount) +- [Download fax file](/docs/api/rest/reference/phone/methods/#operation/Downloadfaxfile) +- [List phone sites](/docs/api/rest/reference/phone/methods/#operation/listPhoneSites) +- [List users in a directory by site](/docs/api/rest/reference/phone/methods/#operation/ListUsersFromDirectoryBySite) +- [Get a shared line group](/docs/api/rest/reference/phone/methods/#operation/getASharedLineGroup) +- [Get a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/phoneUserSettings) +- [Get a phone site setting](/docs/api/rest/reference/phone/methods/#operation/getSiteSettingForType) +- [Get operation logs report](/docs/api/rest/reference/phone/methods/#operation/getPSOperationLogs) +- [Get extension's fax logs](/docs/api/rest/reference/phone/methods/#operation/Getuser'sfaxlogs) +- [Get role information](/docs/api/rest/reference/phone/methods/#operation/getRoleInformation) +- [Get voicemail details](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetails) +- [List setting templates](/docs/api/rest/reference/phone/methods/#operation/listSettingTemplates) +- [Get an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistDetail) +- [Get call QoS](/docs/api/rest/reference/phone/methods/#operation/getCallQoS) +- [List opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/getNumberCampaignOptStatus) +- [List emergency service locations](/docs/api/rest/reference/phone/methods/#operation/listLocations) +- [Get line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/listLineKeySetting) +- [List updatable firmwares](/docs/api/rest/reference/phone/methods/#operation/ListFirmwares) +- [Get call log details](/docs/api/rest/reference/phone/methods/#operation/getCallLogDetails) +- [List account level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listAccountOutboundCallingExceptionRule) +- [List real time location for IP phones](/docs/api/rest/reference/phone/methods/#operation/listPhoneRealtimelocation) +- [Get blocked list details](/docs/api/rest/reference/phone/methods/#operation/getABlockedList) +- [List call logs](/docs/api/rest/reference/phone/methods/#operation/listCallLogsMetrics) +- [Get call element](/docs/api/rest/reference/phone/ma/#operation/getCallElement) +- [Get group phone settings](/docs/api/rest/reference/phone/methods/#operation/getGroupPhoneSettings) +- [List auto receptionists](/docs/api/rest/reference/phone/methods/#operation/listAutoReceptionists) +- [Get phone site details](/docs/api/rest/reference/phone/methods/#operation/getASite) +- [List an extension's inbound block rules](/docs/api/rest/reference/phone/methods/#operation/ListExtensionLevelInboundBlockRules) +- [Get a list of monitoring groups on an account](/docs/api/rest/reference/phone/methods/#operation/listMonitoringGroup) +- [List phone roles](/docs/api/rest/reference/phone/methods/#operation/ListPhoneRoles) +- [Get group policy details](/docs/api/rest/reference/phone/methods/#operation/GetGroupPolicyDetails) +- [List call pickup group members](/docs/api/rest/reference/phone/methods/#operation/listGCPMembers) +- [List common area level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listCommonAreaOutboundCallingExceptionRule) +- [Get common area details](/docs/api/rest/reference/phone/methods/#operation/getACommonArea) +- [Download a phone recording](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingFile) +- [Get site level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetSiteOutboundCallingCountriesAndRegions) +- [Get User AI Call Summary Detail](/docs/api/rest/reference/phone/methods/#operation/getUserAICallSummary) +- [Get auto receptionist IVR](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistIVR) +- [Get account level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetAccountOutboundCallingCountriesAndRegions) +- [List billing accounts](/docs/api/rest/reference/phone/methods/#operation/listBillingAccount) +- [Get account's call history](/docs/api/rest/reference/phone/ma/#operation/accountCallHistory) +- [List site level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listSiteOutboundCallingExceptionRule) +- [Get fax log details](/docs/api/rest/reference/phone/methods/#operation/GetFaxLogDetails) +- [List users' phone numbers for a customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/listUserCustomizeOutboundCallerNumbers) +- [Get setting template details](/docs/api/rest/reference/phone/methods/#operation/getSettingTemplate) +- [Get device details](/docs/api/rest/reference/phone/methods/#operation/getADevice) +- [List customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/listSiteCustomizeOutboundCallerNumbers) +- [Get user level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetUserOutboundCallingCountriesAndRegions) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetailsByCallIdOrCallLogId) +- [List opt statuses of phone numbers assigned to SMS consent](/docs/api/rest/reference/phone/methods/#operation/getNumberConsentOptStatus) +- [List directory backup routing rules](/docs/api/rest/reference/phone/methods/#operation/listRoutingRule) +- [Get user policy details](/docs/api/rest/reference/phone/methods/#operation/GetUserPolicyDetails) +- [List carrier peering phone numbers.](/docs/api/rest/reference/phone/methods/#operation/listCarrierPeeringPhoneNumbers) +- [Sync SMS by session ID](/docs/api/rest/reference/phone/methods/#operation/smsSessionSync) +- [List members in a role](/docs/api/rest/reference/phone/methods/#operation/ListRoleMembers) +- [Sync user's call history](/docs/api/rest/reference/phone/methods/#operation/syncUserCallHistory) +- [Get user's call history](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallHistory) +- [Get fax charges usage report](/docs/api/rest/reference/phone/methods/#operation/Getfaxchargesusagereport) +- [List call queue members](/docs/api/rest/reference/phone/methods/#operation/listCallQueueMembers) +- [Get a user's profile](/docs/api/rest/reference/phone/methods/#operation/phoneUser) +- [List private directory members](/docs/api/rest/reference/phone/methods/#operation/listPrivateDirectoryMembers) +- [List an account's Zoom Phone settings](/docs/api/rest/reference/phone/methods/#operation/listZoomPhoneAccountSettings) +- [List audio items](/docs/api/rest/reference/phone/methods/#operation/ListAudioItems) +- [List call queue analytics](/docs/api/rest/reference/phone/ma/#operation/callqueueanalytics) +- [List call queues](/docs/api/rest/reference/phone/methods/#operation/listCallQueues) +- [List group call pickup objects](/docs/api/rest/reference/phone/methods/#operation/listGCP) +- [List shared line appearances](/docs/api/rest/reference/phone/methods/#operation/listSharedLineAppearances) +- [Get emergency service location details](/docs/api/rest/reference/phone/methods/#operation/getLocation) +- [Get call pickup group by ID](/docs/api/rest/reference/phone/methods/#operation/GetGCP) +- [List activation codes](/docs/api/rest/reference/phone/methods/#operation/listActivationCodes) +- [List phone users](/docs/api/rest/reference/phone/methods/#operation/listPhoneUsers) +- [Get call queue recordings](/docs/api/rest/reference/phone/methods/#operation/getCallQueueRecordings) +- [List Zoom Rooms without Zoom Phone assignment](/docs/api/rest/reference/phone/methods/#operation/listUnassignedZoomRooms) +- [List Smartphones](/docs/api/rest/reference/phone/methods/#operation/ListSmartphones) +- [List nomadic emergency services users](/docs/api/rest/reference/phone/methods/#operation/listUserNomadicEmergencyServices) +- [Get phone account settings](/docs/api/rest/reference/phone/methods/#operation/phoneSetting) +- [List devices](/docs/api/rest/reference/phone/methods/#operation/listPhoneDevices) +- [List SMS campaigns](/docs/api/rest/reference/phone/methods/#operation/listAccountSMSCampaigns) +- [Get account's call logs](/docs/api/rest/reference/phone/methods/#operation/accountCallLogs) +- [List plan information](/docs/api/rest/reference/phone/methods/#operation/listPhonePlans) +- [Get alert setting details](/docs/api/rest/reference/phone/methods/#operation/GetAlertSettingDetails) + +### phone:write + +View and manage your phone information + +**Associated APIs:** + +- [Add an audio item for text-to-speech conversion](/docs/api/rest/reference/phone/methods/#operation/AddAnAudio) +- [Update user's calling plan](/docs/api/rest/reference/phone/methods/#operation/updateCallingPlan) +- [Unassign user's calling plan](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlan) +- [Delete a line key setting.](/docs/api/rest/reference/phone/methods/#operation/DeleteLineKey) +- [Batch update device line key position](/docs/api/rest/reference/phone/methods/#operation/batchUpdateDeviceLineKeySetting) +- [Update Voicemail Read Status](/docs/api/rest/reference/phone/methods/#operation/updateVoicemailReadStatus) +- [Delete a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/deleteUserSetting) +- [Add audio items](/docs/api/rest/reference/phone/methods/#operation/AddAudioItem) +- [Add phone numbers for users' customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/addUserOutboundCallerNumbers) +- [Remove users' customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteUserOutboundCallerNumbers) +- [Delete a user's call history](/docs/api/rest/reference/phone/methods/#operation/deleteUserCallHistory) +- [Update Recording Status](/docs/api/rest/reference/phone/methods/#operation/UpdateRecordingStatus) +- [Delete a user's call log](/docs/api/rest/reference/phone/methods/#operation/deleteCallLog) +- [Update Auto Delete Field](/docs/api/rest/reference/phone/methods/#operation/UpdateAutoDeleteField) +- [Delete an audio item](/docs/api/rest/reference/phone/methods/#operation/DeleteAudioItem) +- [Add an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/AddExtensiontLevelInboundBlockRules) +- [Send fax](/docs/api/rest/reference/phone/methods/#operation/SendEFax) +- [Add a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/addUserSetting) +- [Post SMS message](/docs/api/rest/reference/phone/methods/#operation/postSmsMessage) +- [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/UnassignPhoneNumber) +- [Delete a call recording](/docs/api/rest/reference/phone/methods/#operation/deleteCallRecording) +- [Update a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/updateUserSetting) +- [Delete a voicemail](/docs/api/rest/reference/phone/methods/#operation/deleteVoicemail) +- [Update a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/updateUserSettings) +- [Upload fax file](/docs/api/rest/reference/phone/methods/#operation/UploadFaxFiles) +- [Delete an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/DeleteExtensiontLevelInboundBlockRules) +- [Batch update line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/BatchUpdateLineKeySetting) +- [Update an audio item](/docs/api/rest/reference/phone/methods/#operation/UpdateAudioItem) +- [Update a user's profile](/docs/api/rest/reference/phone/methods/#operation/updateUserProfile) +- [Assign calling plan to a user](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlan) +- [Assign a phone number to a user](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumber) + +### phone:write:admin + +View and manage all users' phone information + +**Associated APIs:** + +- [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/UnassignPhoneNumber) +- [Create a shared line group](/docs/api/rest/reference/phone/methods/#operation/createASharedLineGroup) +- [Send fax](/docs/api/rest/reference/phone/methods/#operation/SendEFax) +- [Update site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateSiteOutboundCallingExceptionRule) +- [Delete an emergency address](/docs/api/rest/reference/phone/methods/#operation/deleteEmergencyAddress) +- [Batch add emergency service locations](/docs/api/rest/reference/phone/methods/#operation/batchAddLocations) +- [Update site level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateSiteOutboundCallingCountriesOrRegions) +- [Delete a phone number](/docs/api/rest/reference/phone/methods/#operation/deleteCRPhoneNumber) +- [Update Auto Delete Field](/docs/api/rest/reference/phone/methods/#operation/UpdateAutoDeleteField) +- [Remove users' customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteUserOutboundCallerNumbers) +- [Update a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/updateZoomRoom) +- [Delete a monitoring group](/docs/api/rest/reference/phone/methods/#operation/deleteMonitoringGroup) +- [Delete common area setting](/docs/api/rest/reference/phone/methods/#operation/deleteCommonAreaSetting) +- [Generate activation codes for common areas](/docs/api/rest/reference/phone/methods/#operation/Generateactivationcodesforcommonareas) +- [Batch update device line key position](/docs/api/rest/reference/phone/methods/#operation/batchUpdateDeviceLineKeySetting) +- [Add an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/AddAccountLevelInboundBlockRules) +- [Unassign an entity from the device](/docs/api/rest/reference/phone/methods/#operation/deleteExtensionFromADevice) +- [Remove peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/deletePeeringPhoneNumbers) +- [Add a setting template](/docs/api/rest/reference/phone/methods/#operation/addSettingTemplate) +- [Delete members in a role](/docs/api/rest/reference/phone/methods/#operation/DelRoleMembers) +- [Delete a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/deleteUserSetting) +- [Assign calling plans to a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlanToRoom) +- [Batch update line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/BatchUpdateLineKeySetting) +- [Add a provision template](/docs/api/rest/reference/phone/methods/#operation/addProvisionTemplate) +- [Delete a device](/docs/api/rest/reference/phone/methods/#operation/deleteADevice) +- [Remove customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteSiteOutboundCallerNumbers) +- [Update an alert setting](/docs/api/rest/reference/phone/methods/#operation/UpdateAnAlertSetting) +- [Delete a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/DeletePolicy) +- [Delete phone role targets](/docs/api/rest/reference/phone/ma/#operation/DeletePhoneRoleTargets) +- [Update opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/updateNumberCampaignOptStatus) +- [Update a provision template](/docs/api/rest/reference/phone/methods/#operation/updateProvisionTemplate) +- [Add BYOC phone numbers](/docs/api/rest/reference/phone/methods/#operation/addBYOCNumber) +- [Update the site setting](/docs/api/rest/reference/phone/methods/#operation/updateSiteSetting) +- [Add members to a monitoring group](/docs/api/rest/reference/phone/methods/#operation/addMembers) +- [Add common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddCommonAreaOutboundCallingExceptionRule) +- [Update common area level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaOutboundCallingCountriesOrRegions) +- [Update Recording Status](/docs/api/rest/reference/phone/methods/#operation/UpdateRecordingStatus) +- [Assign phone numbers](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumbersSLG) +- [Add members to a call pickup group](/docs/api/rest/reference/phone/methods/#operation/addGCPMembers) +- [Remove a member from a monitoring group](/docs/api/rest/reference/phone/methods/#operation/removeMember) +- [Delete an audio item](/docs/api/rest/reference/phone/methods/#operation/DeleteAudioItem) +- [Add members to a call queue](/docs/api/rest/reference/phone/methods/#operation/addMembersToCallQueue) +- [Delete an emergency location](/docs/api/rest/reference/phone/methods/#operation/deleteLocation) +- [Update a setting template](/docs/api/rest/reference/phone/methods/#operation/updateSettingTemplate) +- [Add peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/addPeeringPhoneNumbers) +- [Update common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaOutboundCallingExceptionRule) +- [Delete an external contact](/docs/api/rest/reference/phone/methods/#operation/deleteAExternalContact) +- [Remove a phone number from a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/UnassignPhoneNumberFromZoomRoom) +- [Update user level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateUserOutboundCallingCountriesOrRegions) +- [Update Voicemail Read Status](/docs/api/rest/reference/phone/methods/#operation/updateVoicemailReadStatus) +- [Create a call queue](/docs/api/rest/reference/phone/methods/#operation/createCallQueue) +- [Unassign a member](/docs/api/rest/reference/phone/methods/#operation/unassignMemberFromCallQueue) +- [Update user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateUserOutboundCallingExceptionRule) +- [Update a user's profile](/docs/api/rest/reference/phone/methods/#operation/updateUserProfile) +- [Create a monitoring group](/docs/api/rest/reference/phone/methods/#operation/createMonitoringGroup) +- [Add a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/addUserSetting) +- [Delete users from a directory](/docs/api/rest/reference/phone/methods/#operation/DeleteUsersFromDirectory) +- [Add members to a shared line group](/docs/api/rest/reference/phone/methods/#operation/addMembersToSharedLineGroup) +- [Add a call handling setting](/docs/api/rest/reference/phone/methods/#operation/addCallHandling) +- [Delete a voicemail](/docs/api/rest/reference/phone/methods/#operation/deleteVoicemail) +- [Update user policy](/docs/api/rest/reference/phone/methods/#operation/updateUserPolicy) +- [Unassign a calling plan from the common area](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlansFromCommonArea) +- [Mark a phone number as blocked for all extensions](/docs/api/rest/reference/phone/methods/#operation/MarkPhoneNumberAsBlockedForAllExtensions) +- [Update the group call pickup information](/docs/api/rest/reference/phone/methods/#operation/updateGCP) +- [Add an audio item for text-to-speech conversion](/docs/api/rest/reference/phone/methods/#operation/AddAnAudio) +- [Delete a site setting](/docs/api/rest/reference/phone/methods/#operation/deleteSiteSetting) +- [Update a shared line group](/docs/api/rest/reference/phone/methods/#operation/updateASharedLineGroup) +- [Update phone account settings](/docs/api/rest/reference/phone/methods/#operation/updatePhoneSettings) +- [Update account level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountOutboundCallingCountriesOrRegions) +- [Update phone site details](/docs/api/rest/reference/phone/methods/#operation/updateSiteDetails) +- [Update peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/updatePeeringPhoneNumbers) +- [Delete a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteASharedLineGroup) +- [Update a blocked list](/docs/api/rest/reference/phone/methods/#operation/updateBlockedList) +- [Batch add users](/docs/api/rest/reference/phone/methods/#operation/batchAddUsers) +- [Add a policy subsetting to a call queue](/docs/api/rest/reference/phone/methods/#operation/addCQPolicySubSetting) +- [Update a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/updatePolicy) +- [Delete firmware update rule](/docs/api/rest/reference/phone/methods/#operation/DeleteFirmwareUpdateRule) +- [Update an audio item](/docs/api/rest/reference/phone/methods/#operation/UpdateAudioItem) +- [Reboot a desk phone](/docs/api/rest/reference/phone/methods/#operation/rebootPhoneDevice) +- [Add phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/addOutboundCallerNumbers) +- [Delete a call recording](/docs/api/rest/reference/phone/methods/#operation/deleteCallRecording) +- [Remove a Zoom Room from a ZP account](/docs/api/rest/reference/phone/methods/#operation/RemoveZoomRoom) +- [Activate phone numbers](/docs/api/rest/reference/phone/methods/#operation/activeCRPhoneNumbers) +- [Add members to roles](/docs/api/rest/reference/phone/methods/#operation/AddRoleMembers) +- [Delete a common area](/docs/api/rest/reference/phone/methods/#operation/deleteCommonArea) +- [Add phone role targets](/docs/api/rest/reference/phone/methods/#operation/AddPhoneRoleTargets) +- [Add a client code to a call log](/docs/api/rest/reference/phone/methods/#operation/addClientCodeToCallLog) +- [Unassign phone numbers from common area](/docs/api/rest/reference/phone/methods/#operation/unassignPhoneNumbersFromCommonArea) +- [Update group policy](/docs/api/rest/reference/phone/methods/#operation/updateGroupPolicy) +- [Unassign a member from a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteAMemberSLG) +- [Update a phone number](/docs/api/rest/reference/phone/methods/#operation/updatePhoneNumberDetails) +- [Add a device](/docs/api/rest/reference/phone/methods/#operation/addPhoneDevice) +- [Unassign members from a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteMembersOfSLG) +- [Add audio items](/docs/api/rest/reference/phone/methods/#operation/AddAudioItem) +- [Remove a member from a private directory](/docs/api/rest/reference/phone/methods/#operation/removeAMemberFromAPrivateDirectory) +- [Delete phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/deleteOutboundCallerNumbers) +- [Assign a phone number to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/assignCampaignPhoneNumbers) +- [Add an emergency address](/docs/api/rest/reference/phone/methods/#operation/addEmergencyAddress) +- [Add directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/addRoutingRule) +- [Unassign all phone numbers](/docs/api/rest/reference/phone/methods/#operation/unassignAPhoneNumCallQueue) +- [Update common area](/docs/api/rest/reference/phone/methods/#operation/updateCommonArea) +- [Update account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountOutboundCallingExceptionRule) +- [Delete site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteSiteOutboundCallingExceptionRule) +- [Update a site's unassigned phone numbers](/docs/api/rest/reference/phone/methods/#operation/updateSiteForUnassignedPhoneNumbers) +- [Update an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountLevelInboundBlockRule) +- [Delete a user's call log](/docs/api/rest/reference/phone/methods/#operation/deleteCallLog) +- [Delete a provision template](/docs/api/rest/reference/phone/methods/#operation/deleteProvisionTemplate) +- [Update directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/updateRoutingRule) +- [Add account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddAccountOutboundCallingExceptionRule) +- [Add an external contact](/docs/api/rest/reference/phone/methods/#operation/addExternalContact) +- [Update an auto receptionist policy](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionistPolicy) +- [Create phone numbers](/docs/api/rest/reference/phone/methods/#operation/createCRPhoneNumbers) +- [Assign numbers to a call queue](/docs/api/rest/reference/phone/methods/#operation/assignPhoneToCallQueue) +- [Update a shared line group policy](/docs/api/rest/reference/phone/methods/#operation/updateSharedLineGroupPolicy) +- [Add a policy setting to a shared line group](/docs/api/rest/reference/phone/methods/#operation/addSLGPolicySubSetting) +- [Update emergency service location](/docs/api/rest/reference/phone/methods/#operation/updateLocation) +- [Add a common area](/docs/api/rest/reference/phone/methods/#operation/addCommonArea) +- [Unassign user's calling plan](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlan) +- [Assign calling plan to a user](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlan) +- [Update a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/updateUserSetting) +- [Delete an account's inbound blocked statistics](/docs/api/rest/reference/phone/methods/#operation/DeleteAccountLevelInboundBlockedStatistics) +- [Assign an entity to a device](/docs/api/rest/reference/phone/methods/#operation/addExtensionsToADevice) +- [Create a blocked list](/docs/api/rest/reference/phone/methods/#operation/addAnumberToBlockedList) +- [Update a phone role](/docs/api/rest/reference/phone/methods/#operation/UpdatePhoneRole) +- [Delete a phone site](/docs/api/rest/reference/phone/methods/#operation/deletePhoneSite) +- [Delete an alert setting](/docs/api/rest/reference/phone/methods/#operation/DeleteAnAlertSetting) +- [Add customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/addSiteOutboundCallerNumbers) +- [Delete an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/DeleteAccountLevelInboundBlockRules) +- [Update a call handling setting](/docs/api/rest/reference/phone/methods/#operation/updateCallHandling) +- [Create a phone site](/docs/api/rest/reference/phone/methods/#operation/createPhoneSite) +- [Remove all monitors or monitored members from a monitoring group](/docs/api/rest/reference/phone/methods/#operation/removeMembers) +- [Remove members from call pickup group](/docs/api/rest/reference/phone/methods/#operation/removeGCPMembers) +- [Update a device](/docs/api/rest/reference/phone/methods/#operation/updateADevice) +- [Add a client code to a call history](/docs/api/rest/reference/phone/methods/#operation/addClientCodeToCallHistory) +- [Update common area pin code](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaPinCode) +- [Add an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/AddExtensiontLevelInboundBlockRules) +- [Add common area setting](/docs/api/rest/reference/phone/methods/#operation/AddCommonAreaSetting) +- [Add a site setting](/docs/api/rest/reference/phone/methods/#operation/addSiteSetting) +- [Add users to a directory](/docs/api/rest/reference/phone/methods/#operation/AddUsersToDirectory) +- [Remove a calling plan from a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlanFromRoom) +- [Update multiple users' properties in batch](/docs/api/rest/reference/phone/methods/#operation/updateUsersPropertiesInBatch) +- [Update a monitoring group](/docs/api/rest/reference/phone/methods/#operation/updateMonitoringGroup) +- [Delete a blocked list](/docs/api/rest/reference/phone/methods/#operation/deleteABlockedList) +- [Add an alert setting](/docs/api/rest/reference/phone/methods/#operation/AddAnAlertSetting) +- [Assign phone numbers to a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumberToZoomRoom) +- [Delete unassigned phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteUnassignedPhoneNumbers) +- [Update a private directory member](/docs/api/rest/reference/phone/methods/#operation/updateAPrivateDirectoryMember) +- [Delete directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/deleteRoutingRule) +- [Assign a phone number to a user](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumber) +- [Delete a phone role](/docs/api/rest/reference/phone/methods/#operation/DeletePhoneRole) +- [Post SMS message](/docs/api/rest/reference/phone/methods/#operation/postSmsMessage) +- [Delete an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/DeleteExtensiontLevelInboundBlockRules) +- [Delete a non-primary auto receptionist](/docs/api/rest/reference/phone/methods/#operation/deleteAutoReceptionist) +- [Sync deskphones](/docs/api/rest/reference/phone/methods/#operation/syncPhoneDevice) +- [Update an emergency address](/docs/api/rest/reference/phone/methods/#operation/updateEmergencyAddress) +- [Update an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionist) +- [Update an SLG policy setting](/docs/api/rest/reference/phone/methods/#operation/updateSLGPolicySubSetting) +- [Update account policy](/docs/api/rest/reference/phone/methods/#operation/updateAccountPolicy) +- [Add a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/AddPolicy) +- [Delete a line key setting.](/docs/api/rest/reference/phone/methods/#operation/DeleteLineKey) +- [Delete account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteAccountOutboundCallingExceptionRule) +- [Add site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddSiteOutboundCallingExceptionRule) +- [Delete a user's call history](/docs/api/rest/reference/phone/methods/#operation/deleteUserCallHistory) +- [Apply template to common areas](/docs/api/rest/reference/phone/methods/#operation/ApplyTemplatetoCommonAreas) +- [Delete group call pickup objects](/docs/api/rest/reference/phone/methods/#operation/deleteGCP) +- [Add an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/addAutoReceptionist) +- [Duplicate a phone role](/docs/api/rest/reference/phone/methods/#operation/DuplicatePhoneRole) +- [Add phone numbers for users' customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/addUserOutboundCallerNumbers) +- [Delete a call handling setting](/docs/api/rest/reference/phone/methods/#operation/deleteCallHandling) +- [Upload fax file](/docs/api/rest/reference/phone/methods/#operation/UploadFaxFiles) +- [Update provision template of a device](/docs/api/rest/reference/phone/methods/#operation/updateProvisionTemplateToDevice) +- [Delete common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteCommonAreaOutboundCallingExceptionRule) +- [Assign calling plans to a common area](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlansToCommonArea) +- [Delete users from a directory of a site](/docs/api/rest/reference/phone/methods/#operation/DeleteUsersFromDirectoryBySite) +- [Add a group call pickup object](/docs/api/rest/reference/phone/methods/#operation/addGCP) +- [Add members to a private directory](/docs/api/rest/reference/phone/methods/#operation/addMembersToAPrivateDirectory) +- [Unassign all members](/docs/api/rest/reference/phone/methods/#operation/unassignAllMembers) +- [Add users to a directory of a site](/docs/api/rest/reference/phone/methods/#operation/AddUsersToDirectoryBySite) +- [Update firmware update rule](/docs/api/rest/reference/phone/methods/#operation/UpdateFirmwareRule) +- [Add user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddUserOutboundCallingExceptionRule) +- [Delete user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteUserOutboundCallingExceptionRule) +- [Assign phone numbers to a common area](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumbersToCommonArea) +- [Add a firmware update rule](/docs/api/rest/reference/phone/methods/#operation/AddFirmwareRule) +- [Update common area setting](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaSetting) +- [Update call queue details](/docs/api/rest/reference/phone/methods/#operation/updateCallQueue) +- [Add a Zoom Room to a Zoom Phone](/docs/api/rest/reference/phone/methods/#operation/addZoomRoom) +- [Update external contact](/docs/api/rest/reference/phone/methods/#operation/updateExternalContact) +- [Add an emergency service location](/docs/api/rest/reference/phone/methods/#operation/addLocation) +- [Delete a call queue](/docs/api/rest/reference/phone/methods/#operation/deleteACallQueue) +- [Delete a CQ policy setting](/docs/api/rest/reference/phone/methods/#operation/removeCQPolicySubSetting) +- [Update user's calling plan](/docs/api/rest/reference/phone/methods/#operation/updateCallingPlan) +- [Update a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/updateUserSettings) +- [Update auto receptionist IVR](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionistIVR) +- [Delete an SLG policy setting](/docs/api/rest/reference/phone/methods/#operation/removeSLGPolicySubSetting) +- [Update a call queue's policy subsetting](/docs/api/rest/reference/phone/methods/#operation/updateCQPolicySubSetting) + +### phone_call_log:master + +View and manage all subaccounts' call logs + +**Associated APIs:** + +- [Get user's call logs](/docs/api/rest/reference/phone/ma/#operation/phoneUserCallLogs) +- [Get account's call logs](/docs/api/rest/reference/phone/ma/#operation/accountCallLogs) +- [Get call log details](/docs/api/rest/reference/phone/ma/#operation/getCallLogDetails) +- [Delete a user's call log](/docs/api/rest/reference/phone/ma/#operation/deleteCallLog) +- [Delete a user's call history](/docs/api/rest/reference/phone/ma/#operation/deleteUserCallHistory) + +### phone_call_log:read + +View your call history information + +**Associated APIs:** + +- [Get user's call history](/docs/api/rest/reference/phone/ma/#operation/phoneUserCallHistory) +- [Get user's call logs](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallLogs) +- [Get call element](/docs/api/rest/reference/phone/methods/#operation/getCallElement) +- [Get call history detail](/docs/api/rest/reference/phone/methods/#operation/getCallHistoryDetail) +- [Get call log details](/docs/api/rest/reference/phone/methods/#operation/getCallLogDetails) + +### phone_call_log:read:admin + +View all users' call history information + +**Associated APIs:** + +- [Get account's call history](/docs/api/rest/reference/phone/methods/#operation/accountCallHistory) +- [Get call log details](/docs/api/rest/reference/phone/methods/#operation/getCallLogDetails) +- [Get call history detail](/docs/api/rest/reference/phone/methods/#operation/getCallHistoryDetail) +- [Get user's call logs](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallLogs) +- [Get call element](/docs/api/rest/reference/phone/ma/#operation/getCallElement) +- [Get account's call logs](/docs/api/rest/reference/phone/methods/#operation/accountCallLogs) +- [Get call history](/docs/api/rest/reference/phone/ma/#operation/getCallPath) + +### phone_call_log:write + +View and manage your call log information + +**Associated APIs:** + +- [Delete a user's call log](/docs/api/rest/reference/phone/methods/#operation/deleteCallLog) +- [Delete a user's call history](/docs/api/rest/reference/phone/methods/#operation/deleteUserCallHistory) + +### phone_call_log:write:admin + +View and manage all users' call log information + +**Associated APIs:** + +- [Delete a user's call log](/docs/api/rest/reference/phone/methods/#operation/deleteCallLog) +- [Delete a user's call history](/docs/api/rest/reference/phone/methods/#operation/deleteUserCallHistory) + +### phone_peering:master + +View and manage subaccounts' E164 numbers sent via peering API + +**Associated APIs:** + +- [List peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/listPeeringPhoneNumbers) +- [Add peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/addPeeringPhoneNumbers) +- [Remove peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/deletePeeringPhoneNumbers) +- [Update peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/updatePeeringPhoneNumbers) +- [List carrier peering phone numbers.](/docs/api/rest/reference/phone/ma/#operation/listCarrierPeeringPhoneNumbers) + +### phone_peering:read:admin + +View the E164 numbers sent via peering API + +**Associated APIs:** + +- [List ported numbers](/docs/api/rest/reference/phone/methods/#operation/listPortedNumbers) +- [Get ported numbers details](/docs/api/rest/reference/phone/methods/#operation/getPortedNumbersDetails) +- [List peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/listPeeringPhoneNumbers) +- [List carrier peering phone numbers.](/docs/api/rest/reference/phone/methods/#operation/listCarrierPeeringPhoneNumbers) + +### phone_peering:write:admin + +View and manage the E164 numbers sent via peering API + +**Associated APIs:** + +- [Update peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/updatePeeringPhoneNumbers) +- [Remove peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/deletePeeringPhoneNumbers) +- [Add peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/addPeeringPhoneNumbers) + +### phone_recording:master + +View and manage subaccounts' call recording information + +**Associated APIs:** + +- [Get user's recordings](/docs/api/rest/reference/phone/ma/#operation/phoneUserRecordings) +- [Download a phone recording transcript](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingTranscript) +- [Get call recordings](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordings) +- [Get recording by call ID](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordingsByCallIdOrCallLogId) +- [Download a phone recording](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingFile) + +### phone_recording:read + +View your call recording information + +**Associated APIs:** + +- [Get user's recordings](/docs/api/rest/reference/phone/methods/#operation/phoneUserRecordings) +- [Download a phone recording](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingFile) +- [Get recording by call ID](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordingsByCallIdOrCallLogId) +- [Download a phone recording transcript](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingTranscript) + +### phone_recording:read:admin + +View all users' recording information + +**Associated APIs:** + +- [Download a phone recording transcript](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingTranscript) +- [Get call recordings](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordings) +- [Get meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingGet) +- [Download a phone recording](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingFile) +- [Get recording by call ID](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordingsByCallIdOrCallLogId) +- [Get user's recordings](/docs/api/rest/reference/phone/methods/#operation/phoneUserRecordings) + +### phone_recording:write + +View and manage your call recording information + +**Associated APIs:** + +- [Delete a call recording](/docs/api/rest/reference/phone/methods/#operation/deleteCallRecording) +- [Update Auto Delete Field](/docs/api/rest/reference/phone/methods/#operation/UpdateAutoDeleteField) +- [Update Recording Status](/docs/api/rest/reference/phone/methods/#operation/UpdateRecordingStatus) + +### phone_recording:write:admin + +View and manage all users' call recording information + +**Associated APIs:** + +- [Update Auto Delete Field](/docs/api/rest/reference/phone/methods/#operation/UpdateAutoDeleteField) +- [Delete a call recording](/docs/api/rest/reference/phone/methods/#operation/deleteCallRecording) +- [Update Recording Status](/docs/api/rest/reference/phone/methods/#operation/UpdateRecordingStatus) + +### phone_sms:master + +View and manage all sub accounts' Zoom Phone SMS information + +**Associated APIs:** + +- [Get user's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/userSmsSession) +- [Get account's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/accountSmsSession) +- [Get SMS by message ID](/docs/api/rest/reference/phone/ma/#operation/smsByMessageId) +- [Get SMS session details](/docs/api/rest/reference/phone/ma/#operation/smsSessionDetails) + +### phone_sms:read + +View your Zoom Phone SMS information + +**Associated APIs:** + +- [Get SMS by message ID](/docs/api/rest/reference/phone/methods/#operation/smsByMessageId) +- [Get user's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/userSmsSession) +- [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/methods/#operation/GetSmsSessions) +- [Sync SMS by session ID](/docs/api/rest/reference/phone/methods/#operation/smsSessionSync) +- [Get account's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/accountSmsSession) +- [Get SMS session details](/docs/api/rest/reference/phone/methods/#operation/smsSessionDetails) + +### phone_sms:read:admin + +View all users' Zoom Phone SMS information + +**Associated APIs:** + +- [Get user's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/userSmsSession) +- [Get account's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/accountSmsSession) +- [Get SMS session details](/docs/api/rest/reference/phone/methods/#operation/smsSessionDetails) +- [Get SMS by message ID](/docs/api/rest/reference/phone/methods/#operation/smsByMessageId) +- [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/methods/#operation/GetSmsSessions) +- [Sync SMS by session ID](/docs/api/rest/reference/phone/methods/#operation/smsSessionSync) + +### phone_sms:write + +View and manage your Zoom Phone SMS information + +**Associated APIs:** + +- [Post SMS message](/docs/api/rest/reference/phone/methods/#operation/postSmsMessage) + +### phone_sms:write:admin + +View and manage all users' Zoom Phone SMS information + +**Associated APIs:** + +- [Post SMS message](/docs/api/rest/reference/phone/methods/#operation/postSmsMessage) + +### phone_voicemail:master + +View and manage subaccounts' call voicemails + +**Associated APIs:** + +- [Download a phone voicemail](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadVoicemailFile) +- [Delete a voicemail](/docs/api/rest/reference/phone/ma/#operation/deleteVoicemail) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetailsByCallIdOrCallLogId) +- [Get voicemail details](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetails) +- [Get account voicemails](/docs/api/rest/reference/phone/ma/#operation/accountVoiceMails) +- [Get user's voicemails](/docs/api/rest/reference/phone/ma/#operation/phoneUserVoiceMails) + +### phone_voicemail:read + +View your call voicemail information + +**Associated APIs:** + +- [Get voicemail details](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetails) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetailsByCallIdOrCallLogId) +- [Get user's voicemails](/docs/api/rest/reference/phone/methods/#operation/phoneUserVoiceMails) +- [Download a phone voicemail](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadVoicemailFile) + +### phone_voicemail:read:admin + +View all users' call voicemail information + +**Associated APIs:** + +- [Get account voicemails](/docs/api/rest/reference/phone/methods/#operation/accountVoiceMails) +- [Download a phone voicemail](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadVoicemailFile) +- [Get voicemail details](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetails) +- [Get user's voicemails](/docs/api/rest/reference/phone/methods/#operation/phoneUserVoiceMails) +- [Get user voicemail details from a call log](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetailsByCallIdOrCallLogId) + +### phone_voicemail:write + +View and manage your call voicemail information + +**Associated APIs:** + +- [Delete a voicemail](/docs/api/rest/reference/phone/methods/#operation/deleteVoicemail) +- [Update Voicemail Read Status](/docs/api/rest/reference/phone/methods/#operation/updateVoicemailReadStatus) + +### phone_voicemail:write:admin + +View and manage all users' call voicemail information + +**Associated APIs:** + +- [Delete a voicemail](/docs/api/rest/reference/phone/methods/#operation/deleteVoicemail) +- [Update Voicemail Read Status](/docs/api/rest/reference/phone/methods/#operation/updateVoicemailReadStatus) + +## QualityManagement + +### qm_evaluations:read + +Read evaluations + +### qm_evaluations:read:admin + +Read evaluations + +### qm_interactions:read + +View interactions + +### qm_interactions:read:admin + +View interactions + +## Recording + +### recording:master + +View and manage sub account's user recordings + +**Associated APIs:** + +- [List all recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingsList) +- [List archived files](/docs/api/rest/reference/zoom-api/ma/#operation/listArchivedFiles) +- [List an account's recordings](/docs/api/rest/reference/zoom-api/ma/#operation/ListRecordingsOfAnAccount) +- [Delete a recording file for a meeting or webinar](/docs/api/rest/reference/zoom-api/ma/#operation/recordingDeleteOne) +- [Recover meeting recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingStatusUpdate) +- [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/recordingRegistrantQuestionUpdate) +- [Create a recording registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrantCreate) +- [Recover a single recording](/docs/api/rest/reference/zoom-api/ma/#operation/recordingStatusUpdateOne) +- [Get meeting recording settings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingSettingUpdate) +- [Delete meeting or webinar recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingDelete) +- [Update a registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrantStatus) +- [Update meeting recording settings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingSettingsUpdate) +- [Get meeting recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingGet) +- [List recording registrants](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrants) +- [Get registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/recordingRegistrantsQuestionsGet) + +### recording:read + +View your recordings + +**Associated APIs:** + +- [Get a meeting transcript](/docs/api/rest/reference/zoom-api/methods/#operation/GetMeetingTranscript) +- [Get meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingGet) +- [Get registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantsQuestionsGet) +- [List recording registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrants) +- [List all recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingsList) +- [Get a meeting's archived files](/docs/api/rest/reference/zoom-api/methods/#operation/getArchivedFiles) +- [Get meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingUpdate) + +### recording:read:admin + +View all user recordings + +**Associated APIs:** + +- [Get a meeting transcript](/docs/api/rest/reference/zoom-api/methods/#operation/GetMeetingTranscript) +- [List recording registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrants) +- [Get meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingUpdate) +- [List all recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingsList) +- [List archived files](/docs/api/rest/reference/zoom-api/methods/#operation/listArchivedFiles) +- [Get registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantsQuestionsGet) +- [Get archived file statistics](/docs/api/rest/reference/zoom-api/methods/#operation/getArchivedFileStatistics) + +### recording:write + +View and manage your recordings + +**Associated APIs:** + +- [Create a recording registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantCreate) +- [Update a registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantStatus) +- [Delete a recording file for a meeting or webinar](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDeleteOne) +- [Update an archived file's auto-delete status](/docs/api/rest/reference/zoom-api/methods/#operation/updateArchivedFile) +- [Recover meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdate) +- [Update meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingsUpdate) +- [Delete a meeting or webinar transcript](/docs/api/rest/reference/zoom-api/methods/#operation/DeleteMeetingTranscript) +- [Recover a single recording](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdateOne) +- [Get a meeting transcript](/docs/api/rest/reference/zoom-api/methods/#operation/GetMeetingTranscript) +- [Delete meeting or webinar recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDelete) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantQuestionUpdate) + +### recording:write:admin + +View and manage all user recordings + +**Associated APIs:** + +- [Recover a single recording](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdateOne) +- [Delete meeting or webinar recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDelete) +- [Update an archived file's auto-delete status](/docs/api/rest/reference/zoom-api/methods/#operation/updateArchivedFile) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantQuestionUpdate) +- [Create a recording registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantCreate) +- [Update meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingsUpdate) +- [Delete a meeting's archived files](/docs/api/rest/reference/zoom-api/methods/#operation/deleteArchivedFiles) +- [Update a registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantStatus) +- [Delete a recording file for a meeting or webinar](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDeleteOne) +- [Recover meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdate) +- [Get a meeting transcript](/docs/api/rest/reference/zoom-api/methods/#operation/GetMeetingTranscript) +- [Delete a meeting or webinar transcript](/docs/api/rest/reference/zoom-api/methods/#operation/DeleteMeetingTranscript) + +## Report + +### report:master + +View sub account's report data + +**Associated APIs:** + +- [Get webinar participant reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarParticipants) +- [Get meeting reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetings) +- [Get webinar Q&A report](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarQA) +- [Get telephone reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportTelephone) +- [Get operation logs report](/docs/api/rest/reference/zoom-api/ma/#operation/reportOperationLogs) +- [Get remote support report](/docs/api/rest/reference/zoom-api/ma/#operation/Getremotesupportreport) +- [Get sign In / sign out activity report](/docs/api/rest/reference/zoom-api/ma/#operation/reportSignInSignOutActivities) +- [Get webinar poll reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarPolls) +- [Get upcoming events reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportUpcomingEvents) +- [Get a meeting activities report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingactivitylogs) +- [Get cloud recording usage report](/docs/api/rest/reference/zoom-api/ma/#operation/reportCloudRecording) +- [Get meeting Q&A report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingQA) +- [Get daily usage report](/docs/api/rest/reference/zoom-api/ma/#operation/reportDaily) +- [Get webinar detail reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarDetails) +- [Get billing invoice reports](/docs/api/rest/reference/zoom-api/ma/#operation/getBillingInvoicesReports) +- [Get meeting poll reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingPolls) +- [Get meeting participant reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingParticipants) +- [Get history meeting and webinar list](/docs/api/rest/reference/zoom-api/ma/#operation/Gethistorymeetingandwebinarlist) +- [Get billing reports](/docs/api/rest/reference/zoom-api/ma/#operation/getBillingReport) +- [Get meeting survey report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingSurvey) +- [Get meeting detail reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingDetails) +- [Get active or inactive host reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportUsers) + +### report:read:admin + +View report data + +**Associated APIs:** + +- [Get upcoming events reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportUpcomingEvents) +- [Get meeting detail reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingDetails) +- [Get sign In / sign out activity report](/docs/api/rest/reference/zoom-api/methods/#operation/reportSignInSignOutActivities) +- [Get remote support report](/docs/api/rest/reference/zoom-api/methods/#operation/Getremotesupportreport) +- [Get billing reports](/docs/api/rest/reference/zoom-api/methods/#operation/getBillingReport) +- [Get meeting survey report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingSurvey) +- [Get daily usage report](/docs/api/rest/reference/zoom-api/methods/#operation/reportDaily) +- [Get billing invoice reports](/docs/api/rest/reference/zoom-api/methods/#operation/getBillingInvoicesReports) +- [Get webinar Q&A report](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarQA) +- [Get active or inactive host reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportUsers) +- [Get meeting participant reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingParticipants) +- [Get meeting poll reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingPolls) +- [Get webinar survey report](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarSurvey) +- [Get webinar detail reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarDetails) +- [Get telephone reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportTelephone) +- [Get history meeting and webinar list](/docs/api/rest/reference/zoom-api/methods/#operation/Gethistorymeetingandwebinarlist) +- [Get meeting Q&A report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingQA) +- [Get webinar participant reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarParticipants) +- [Get cloud recording usage report](/docs/api/rest/reference/zoom-api/methods/#operation/reportCloudRecording) +- [Get meeting reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetings) +- [Get webinar poll reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarPolls) +- [Get a meeting activities report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingactivitylogs) +- [Get operation logs report](/docs/api/rest/reference/zoom-api/methods/#operation/reportOperationLogs) + +### report_chat:read:admin + +View your team chat history report + +**Associated APIs:** + +- [Get chat message reports](/docs/api/rest/reference/chat/methods/#operation/reportChatMessages) +- [Get chat sessions reports](/docs/api/rest/reference/chat/methods/#operation/reportChatSessions) + +### zva_report:read:admin + +View zva engagements + +**Associated APIs:** + +- [Get ZVA transcripts](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVATranscripts) +- [Get ZVA variable details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAengagementvariabledetails) +- [Get ZVA query details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAQueryDetails) +- [Get ZVA Surveys](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVASurveys) +- [Get ZVA engagements](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAEngagements) + +## Role + +### role:master + +View and manage sub account user roles + +**Associated APIs:** + +- [Assign a role](/docs/api/rest/reference/account/ma/#operation/AddRoleMembers) +- [Get role information](/docs/api/rest/reference/account/ma/#operation/getRoleInformation) +- [Create a role](/docs/api/rest/reference/account/ma/#operation/createRole) +- [Update role information](/docs/api/rest/reference/account/ma/#operation/updateRole) +- [List members in a role](/docs/api/rest/reference/account/ma/#operation/roleMembers) +- [List roles](/docs/api/rest/reference/account/ma/#operation/roles) +- [Unassign a role](/docs/api/rest/reference/account/ma/#operation/roleMemberDelete) +- [Delete a role](/docs/api/rest/reference/account/ma/#operation/deleteRole) + +### role:read:admin + +View all user roles + +**Associated APIs:** + +- [List members in a role](/docs/api/rest/reference/account/methods/#operation/roleMembers) +- [List roles](/docs/api/rest/reference/account/methods/#operation/roles) +- [Get role information](/docs/api/rest/reference/account/methods/#operation/getRoleInformation) + +### role:write:admin + +View and manage all user roles + +**Associated APIs:** + +- [List roles](/docs/api/rest/reference/account/methods/#operation/roles) +- [Update role information](/docs/api/rest/reference/account/methods/#operation/updateRole) +- [Delete a role](/docs/api/rest/reference/account/methods/#operation/deleteRole) +- [Unassign a role](/docs/api/rest/reference/account/methods/#operation/roleMemberDelete) +- [Assign a role](/docs/api/rest/reference/account/methods/#operation/AddRoleMembers) +- [Get role information](/docs/api/rest/reference/account/methods/#operation/getRoleInformation) +- [Create a role](/docs/api/rest/reference/account/methods/#operation/createRole) +- [List members in a role](/docs/api/rest/reference/account/methods/#operation/roleMembers) + +## Room + +### room:master + +View and manage sub account's Zoom Rooms information + +**Associated APIs:** + +- [Get Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Getdigitalsignagecontentfolderdetails) +- [Get device information](/docs/api/rest/reference/zoom-rooms/ma/#operation/getRoomDevices) +- [Change a Zoom Room's location](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeZRLocation) +- [List Digital Signage library playlists](/docs/api/rest/reference/zoom-rooms/ma/#operation/ListDigitalSignagelibraryplaylists) +- [Create a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/createRoomDeviceProfile) +- [Get Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/GetDigitalSignagelibraryplaylistcontentitems) +- [Get Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/GetDigitalSignagelibraryplaylist) +- [Get Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationProfile) +- [Update Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRLocationProfile) +- [Get Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRAccountProfile) +- [Delete a location](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteAZRLocation) +- [Get Digital Signage content item](/docs/api/rest/reference/zoom-rooms/ma/#operation/Getdigitalsignagecontentitem) +- [Get Zoom Room settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRSettings) +- [Delete a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteRoomProfile) +- [List digital signage contents](/docs/api/rest/reference/zoom-rooms/ma/#operation/listDigitalSignageContent) +- [Get Zoom Rooms virtual controller URL](/docs/api/rest/reference/zoom-rooms/ma/#operation/getWebzrcUrl) +- [Add a digital signage URL](/docs/api/rest/reference/zoom-rooms/ma/#operation/AddadigitalsignageURL) +- [Get location settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationSettings) +- [List Zoom Rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZoomRooms) +- [List Digital Signage content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/GETListdigitalsignagecontentitems) +- [Update Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRAccProfile) +- [Use Zoom Room controls](/docs/api/rest/reference/zoom-rooms/ma/#operation/ZoomRoomsControls) +- [List device profiles](/docs/api/rest/reference/zoom-rooms/ma/#operation/getRoomProfiles) +- [Update Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZoomRoomAccSettings) +- [Update Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateDigitalSignagelibraryplaylistcontentitems) +- [Get Zoom Room location structure](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationStructure) +- [Update location settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRLocationSettings) +- [Change Zoom Rooms app version](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeZoomRoomsAppVersion) +- [Delete a Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Deleteadigitalsignagecontentfolder) +- [Change the assigned parent location](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeParentLocation) +- [Add a location](/docs/api/rest/reference/zoom-rooms/ma/#operation/addAZRLocation) +- [Update a digital signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignagecontentfolder) +- [Update E911 digital signage](/docs/api/rest/reference/zoom-rooms/ma/#operation/manageE911signage) +- [Add a digital signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Addadigitalsignagecontentfolder) +- [Add a Zoom Room](/docs/api/rest/reference/zoom-rooms/ma/#operation/addARoom) +- [Add a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/AddaDigitalSignagelibraryplaylist) +- [Config Zoom Room Controller Apps](/docs/api/rest/reference/zoom-rooms/methods/#operation/ConfigZoomRoomControllerApps) +- [Delete Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/DeleteDigitalSignagelibraryplaylist) +- [Delete a Digital Signage content item](/docs/api/rest/reference/zoom-rooms/ma/#operation/Deleteadigitalsignagecontentitem) +- [Delete a Zoom Room](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteAZoomRoom) +- [Update Zoom Room settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRSettings) +- [Update Zoom Rooms location structure](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZoomRoomsLocationStructure) +- [List Zoom Room locations](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZRLocations) +- [Get Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRAccountSettings) +- [Update a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateDeviceProfile) +- [Update a Digital Signage content item attributes](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignagecontentitemattributes) +- [Get a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getRoomProfile) +- [Update a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateaDigitalSignagelibraryplaylist) +- [List Zoom Room devices](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZRDevices) + +### room:read:admin + +View all users' Zoom Rooms information + +**Associated APIs:** + +- [Get Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getdigitalsignagecontentfolderdetails) +- [Get device information](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomDevices) +- [List released workspaces by timeout](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorksapceReservationReleaseInof) +- [Get Zoom Rooms virtual controller URL](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWebzrcUrl) +- [List digital signage contents](/docs/api/rest/reference/zoom-rooms/methods/#operation/listDigitalSignageContent) +- [Get Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetDigitalSignagelibraryplaylist) +- [List Digital Signage library playlists](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDigitalSignagelibraryplaylists) +- [Get Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRAccountProfile) +- [Get Digital Signage content item](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getdigitalsignagecontentitem) +- [List Zoom Rooms background image library contents](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListZoomRoomsbackgroundimagelibrarycontents) +- [Get Zoom Room settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRSettings) +- [Get Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetDigitalSignagelibraryplaylistcontentitems) +- [List Zoom Room devices](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZRDevices) +- [Get Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRAccountSettings) +- [Get Zoom Room sensor data](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRSensorData) +- [Get Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetZoomRoomsBackgroundImageLibraryContent) +- [Get a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomProfile) +- [Get a calendar resource by ID](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarResourceById) +- [List Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDigitalSignagelibraryplaylistpublishedrooms) +- [List all Zoom Room Tags](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZoomRoomTags) +- [List device profiles](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomProfiles) +- [List default Zoom Rooms background image library contents](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDefaultZoomRoomsBackgroundImageLibrarycontents) +- [Get Zoom Room profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRProfile) +- [List Zoom Room locations](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZRLocations) +- [List calendar services](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarServices) +- [Get Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetZoomRoomsBackgroundLibraryFolder) +- [List Zoom Rooms Background Image Library Folders](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListZoomRoomsBackgroundLibraryFolders) +- [List Zoom Rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZoomRooms) +- [List Digital Signage content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/GETListdigitalsignagecontentitems) +- [List calendar resources by calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarResourcesByServiceId) +- [Get Zoom Room location structure](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationStructure) +- [Get Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationProfile) +- [Get location settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationSettings) + +### room:write:admin + +View and manage all users' Zoom Rooms information + +**Associated APIs:** + +- [Delete a Digital Signage content item](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadigitalsignagecontentitem) +- [Delete a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteAZoomRoom) +- [Update Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRAccProfile) +- [Delete a calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteACalendarService) +- [Update location settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRLocationSettings) +- [Change Zoom Rooms app version](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeZoomRoomsAppVersion) +- [Update a Zoom Room profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateRoomProfile) +- [Add a calendar resource to a calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/addACalendarResourceToCalendarService) +- [Update Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateDigitalSignagelibraryplaylistpublishedrooms) +- [Add a digital signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Addadigitalsignagecontentfolder) +- [Add a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddaDigitalSignagelibraryplaylist) +- [Delete Zoom Rooms Background Image Library Content](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteZoomRoomsBackgroundImageLibraryContent) +- [Un-assign Tags from a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/unassignZoomRoomTag) +- [Config Zoom Room Controller Apps](/docs/api/rest/reference/zoom-rooms/methods/#operation/ConfigZoomRoomControllerApps) +- [Delete Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteDigitalSignagelibraryplaylist) +- [Add a digital signage URL](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddadigitalsignageURL) +- [Update Zoom Room settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRSettings) +- [Update Zoom Rooms location structure](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZoomRoomsLocationStructure) +- [Update Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateDigitalSignagelibraryplaylistcontentitems) +- [Start calendar service sync process](/docs/api/rest/reference/zoom-rooms/methods/#operation/syncACalendarService) +- [Create a new Zoom Rooms Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/createZoomRoomTag) +- [Create a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/createRoomDeviceProfile) +- [Delete Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteZoomRoomsBackgroundLibraryFolder) +- [Delete a Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadigitalsignagecontentfolder) +- [Change a Zoom Room's location](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeZRLocation) +- [Update Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZoomRoomAccSettings) +- [Delete a calendar resource](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteACalendarResource) +- [Update a Digital Signage image or video file](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignageimageorvideofile) +- [Add a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/addARoom) +- [Add a digital signage image or video](/docs/api/rest/reference/zoom-rooms/methods/#operation/Adddigitalsignageimageorvideo) +- [Use Zoom Room controls](/docs/api/rest/reference/zoom-rooms/methods/#operation/ZoomRoomsControls) +- [Update a digital signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignagecontentfolder) +- [Update a Digital Signage content item attributes](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignagecontentitemattributes) +- [Add a location](/docs/api/rest/reference/zoom-rooms/methods/#operation/addAZRLocation) +- [Update E911 digital signage](/docs/api/rest/reference/zoom-rooms/methods/#operation/manageE911signage) +- [Delete a Zoom Room user device](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteDevice) +- [Assign Tags to Zoom Rooms By Location ID](/docs/api/rest/reference/zoom-rooms/methods/#operation/AssignTagsToZoomRoomsByLocationID) +- [Delete a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteRoomProfile) +- [Delete a location](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteAZRLocation) +- [Delete Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteZoomRoomTag) +- [Update a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateDeviceProfile) +- [Get a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomProfile) +- [Update Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateZoomRoomsBackgroundImageLibraryContent) +- [Update a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateaDigitalSignagelibraryplaylist) +- [Edit Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/editZoomRoomTag) +- [Update Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRLocationProfile) +- [Add Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddZoomRoomsBackgroundLibraryFolder) +- [Change the assigned parent location](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeParentLocation) +- [Assign Tags to a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/AssignTagsToAZoomRoom) +- [Update Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateaZoomRoomsBackgroundLibraryFolderName) +- [Add Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddZoomRoomsBackgroundImageLibraryContent) + +## Rooms Web + +### room:read:master + +View sub account's Zoom Rooms information + +**Associated APIs:** + +- [List Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/ListDigitalSignagelibraryplaylistpublishedrooms) + +### room:write:master + +Manage sub account's Zoom Rooms information + +**Associated APIs:** + +- [Update Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateDigitalSignagelibraryplaylistpublishedrooms) +- [Add a digital signage image or video](/docs/api/rest/reference/zoom-rooms/ma/#operation/Adddigitalsignageimageorvideo) +- [Delete a Digital Signage content item](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadigitalsignagecontentitem) +- [Update a Digital Signage image or video file](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignageimageorvideofile) + +## SCIM2 + +### scim2 + +Call Zoom SCIM2 API + +**Associated APIs:** + +- [Get a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Get) +- [List users](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2List) +- [List groups](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2List) +- [Delete a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Delete) +- [Create a group](/docs/api/rest/reference/scim-api/methods/#operation/groupScim2Create) +- [Update a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Update) +- [Deactivate a user](/docs/api/rest/reference/scim-api/methods/#operation/userADSCIM2Deactivate) +- [Get a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Get) +- [Update a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Update) +- [Create a user](/docs/api/rest/reference/scim-api/methods/#operation/userScim2Create) +- [Delete a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Delete) + +## SIP + +### sip_phone:master + +View and manage sub account's SIP phone information + +**Associated APIs:** + +- [Delete SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/deleteSIPPhonePhones) +- [Enable SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/EnableSIPPhonePhones) +- [Update SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/UpdateSIPPhonePhones) +- [List SIP phones](/docs/api/rest/reference/zoom-api/ma/#operation/ListSIPPhonePhones) + +### sip_phone:read:admin + +View all users' SIP phone information + +**Associated APIs:** + +- [List SIP phones](/docs/api/rest/reference/zoom-api/methods/#operation/ListSIPPhonePhones) + +### sip_phone:write:admin + +View and manage all users' SIP phone information + +**Associated APIs:** + +- [Delete SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/deleteSIPPhonePhones) +- [Update SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/UpdateSIPPhonePhones) +- [Enable SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/EnableSIPPhonePhones) + +### sip_trunk:master + +View and manage sub account's SIP trunk information + +**Associated APIs:** + +- [Delete a SIP trunk](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteASIPTrunk) +- [Delete all numbers](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteAllNumbers) +- [Update an internal number](/docs/api/rest/reference/zoom-api/ma/#operation/UpdateAnInternalNumber) +- [Assign SIP trunk configuration](/docs/api/rest/reference/zoom-api/ma/#operation/AssignSIPTrunkConfiguration) +- [Get SIP trunk configuration](/docs/api/rest/reference/zoom-api/ma/#operation/GetSIPTrunkConfiguration) +- [Assign numbers](/docs/api/rest/reference/zoom-api/ma/#operation/AssignNumbers) +- [Delete internal call-out country](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteInternalCall-outCountry) +- [Add internal numbers](/docs/api/rest/reference/zoom-api/ma/#operation/AddInternalNumbers) +- [Delete an internal number](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteAnInternalNumber) +- [List SIP trunks](/docs/api/rest/reference/zoom-api/ma/#operation/ListSIPTrunks) +- [Add internal call-out countries](/docs/api/rest/reference/zoom-api/ma/#operation/AddInternalCall-outCountries) +- [Assign SIP trunks](/docs/api/rest/reference/zoom-api/ma/#operation/AssignSIPTrunks) +- [List SIP trunk numbers](/docs/api/rest/reference/zoom-api/ma/#operation/listSipTrunkNumbers) +- [List internal numbers](/docs/api/rest/reference/zoom-api/ma/#operation/ListInternalNumbers) +- [List internal call-out countries](/docs/api/rest/reference/zoom-api/ma/#operation/ListInternalCall-outCountries) + +## SMS + +### contact_center_sms:write:admin + +send sms + +**Associated APIs:** + +- [Send an SMS](/docs/api/rest/reference/contact-center/methods/#operation/contactCenterSMS) + +## Scheduler + +### scheduler:read + +View scheduler + +**Associated APIs:** + +- [List schedules](/docs/api/rest/reference/phone/methods/#operation/list_schedules) +- [List scheduled events](/docs/api/rest/reference/phone/methods/#operation/list_scheduled_events) +- [get routing response](/docs/api/rest/reference/phone/methods/#operation/Getroutingresponse) +- [List availability](/docs/api/rest/reference/phone/methods/#operation/list_availability) +- [Get scheduled event attendee](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_event_attendee) +- [Report analytics](/docs/api/rest/reference/phone/methods/#operation/report_analytics) +- [List team](/docs/api/rest/reference/phone/methods/#operation/Listteam) +- [Get user](/docs/api/rest/reference/phone/methods/#operation/get_user) +- [Get availability](/docs/api/rest/reference/phone/methods/#operation/get_availability) +- [Get schedules](/docs/api/rest/reference/phone/methods/#operation/get_schedule) +- [Get scheduled events](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_events) + +### scheduler:read:admin + +View scheduler + +**Associated APIs:** + +- [Get availability](/docs/api/rest/reference/phone/methods/#operation/get_availability) +- [Get scheduled event attendee](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_event_attendee) +- [List team](/docs/api/rest/reference/phone/methods/#operation/Listteam) +- [Get schedules](/docs/api/rest/reference/phone/methods/#operation/get_schedule) +- [Get user](/docs/api/rest/reference/phone/methods/#operation/get_user) +- [Get scheduled events](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_events) +- [List schedules](/docs/api/rest/reference/phone/methods/#operation/list_schedules) +- [List scheduled events](/docs/api/rest/reference/phone/methods/#operation/list_scheduled_events) +- [Report analytics](/docs/api/rest/reference/phone/methods/#operation/report_analytics) +- [get routing response](/docs/api/rest/reference/phone/methods/#operation/Getroutingresponse) +- [List availability](/docs/api/rest/reference/phone/methods/#operation/list_availability) + +### scheduler:write + +Manage scheduler + +**Associated APIs:** + +- [Create shares](/docs/api/rest/reference/phone/methods/#operation/create_shares) +- [Delete scheduled events](/docs/api/rest/reference/phone/methods/#operation/delete_scheduled_events) +- [Patch schedules](/docs/api/rest/reference/phone/methods/#operation/patch_schedule) +- [Insert schedules](/docs/api/rest/reference/phone/methods/#operation/insert_schedule) +- [Patch scheduled events](/docs/api/rest/reference/phone/methods/#operation/patch_scheduled_events) +- [Patch availability](/docs/api/rest/reference/phone/methods/#operation/patch_availability) +- [Insert availability](/docs/api/rest/reference/phone/methods/#operation/insert_availability) +- [Delete schedules](/docs/api/rest/reference/phone/methods/#operation/delete_schedules) +- [Delete availability](/docs/api/rest/reference/phone/methods/#operation/delete_availability) +- [Single use link](/docs/api/rest/reference/phone/methods/#operation/single_use_link) + +### scheduler:write:admin + +Manage scheduler + +**Associated APIs:** + +- [Insert availability](/docs/api/rest/reference/phone/methods/#operation/insert_availability) +- [Patch availability](/docs/api/rest/reference/phone/methods/#operation/patch_availability) +- [Delete availability](/docs/api/rest/reference/phone/methods/#operation/delete_availability) +- [Delete scheduled events](/docs/api/rest/reference/phone/methods/#operation/delete_scheduled_events) +- [Single use link](/docs/api/rest/reference/phone/methods/#operation/single_use_link) +- [Delete schedules](/docs/api/rest/reference/phone/methods/#operation/delete_schedules) +- [Create shares](/docs/api/rest/reference/phone/methods/#operation/create_shares) +- [Insert schedules](/docs/api/rest/reference/phone/methods/#operation/insert_schedule) +- [Patch scheduled events](/docs/api/rest/reference/phone/methods/#operation/patch_scheduled_events) +- [Patch schedules](/docs/api/rest/reference/phone/methods/#operation/patch_schedule) + +## Special + +### app:deeplink:write + +Generate an app deeplink + +**Associated APIs:** + +- [Generate an app deeplink](/docs/api/rest/reference/marketplace/methods/#operation/generateAppDeeplink) + +### app:deeplink:write:admin + +Generate an app deeplink + +**Associated APIs:** + +- [Generate an app deeplink](/docs/api/rest/reference/marketplace/methods/#operation/generateAppDeeplink) + +## Special&ChatBot + +### app:notification:read + +View chat app notification + +**Associated APIs:** + +- [Send app notifications](/docs/api/rest/reference/marketplace/methods/#operation/Sendappnotifications) + +### app:notification:write + +View and Manage chat app notification + +**Associated APIs:** + +- [Send app notifications](/docs/api/rest/reference/marketplace/methods/#operation/Sendappnotifications) + +## Survey Management + +### survey:master + +View survey information + +**Associated APIs:** + +- [Get survey info](/docs/api/rest/reference/account/ma/#operation/getSurveyInfo) +- [Get survey answers](/docs/api/rest/reference/account/ma/#operation/getSurveyAnswers) +- [Get survey instances](/docs/api/rest/reference/account/ma/#operation/getSurveyInstancesInfo) +- [Get surveys](/docs/api/rest/reference/account/ma/#operation/getAccountSurveys) + +### survey:read:admin + +View survey information + +**Associated APIs:** + +- [Get survey answers](/docs/api/rest/reference/account/methods/#operation/getSurveyAnswers) +- [Get survey info](/docs/api/rest/reference/account/methods/#operation/getSurveyInfo) +- [Get survey instances](/docs/api/rest/reference/account/methods/#operation/getSurveyInstancesInfo) +- [Get surveys](/docs/api/rest/reference/account/methods/#operation/getAccountSurveys) + +## TSP + +### tsp:master + +View and manage sub account's TSP account info + +**Associated APIs:** + +- [Set global dial-in URL for a TSP user](/docs/api/rest/reference/zoom-api/ma/#operation/tspUrlUpdate) +- [Update a TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPUpdate) +- [Delete a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPDelete) +- [Get account's TSP information](/docs/api/rest/reference/zoom-api/ma/#operation/tsp) +- [Get a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSP) +- [List user's TSP accounts](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPs) +- [Update an account's TSP information](/docs/api/rest/reference/zoom-api/ma/#operation/tspUpdate) +- [Add a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPCreate) + +### tsp:read + +View your TSP account info + +**Associated APIs:** + +- [List user's TSP accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPs) +- [Get a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSP) + +### tsp:read:admin + +View TSP info + +**Associated APIs:** + +- [Get a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSP) +- [List user's TSP accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPs) +- [Get account's TSP information](/docs/api/rest/reference/zoom-api/methods/#operation/tsp) + +### tsp:write + +View and manage your TSP account info + +**Associated APIs:** + +- [Delete a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPDelete) +- [Update a TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPUpdate) +- [Add a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPCreate) +- [Set global dial-in URL for a TSP user](/docs/api/rest/reference/zoom-api/methods/#operation/tspUrlUpdate) + +### tsp:write:admin + +View and manage TSP info + +**Associated APIs:** + +- [Set global dial-in URL for a TSP user](/docs/api/rest/reference/zoom-api/methods/#operation/tspUrlUpdate) +- [Update an account's TSP information](/docs/api/rest/reference/zoom-api/methods/#operation/tspUpdate) +- [Delete a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPDelete) +- [Add a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPCreate) +- [Update a TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPUpdate) + +## Tasks + +### tasks:delete + +Delete user's task(s) + +**Associated APIs:** + +- [Delete a task](/docs/api/rest/reference/Tasks/methods/#operation/deleteTask) + +### tasks:delete:admin + +Delete account's task(s) + +**Associated APIs:** + +- [Delete a task](/docs/api/rest/reference/Tasks/methods/#operation/deleteTask) + +### tasks:read + +Get user’s task(s) + +**Associated APIs:** + +- [Get task details](/docs/api/rest/reference/Tasks/methods/#operation/getTaskDetail) +- [List tasks](/docs/api/rest/reference/Tasks/methods/#operation/getMyTasks) + +### tasks:read:admin + +Get account's task(s) + +**Associated APIs:** + +- [List tasks](/docs/api/rest/reference/Tasks/methods/#operation/getMyTasks) +- [Get task details](/docs/api/rest/reference/Tasks/methods/#operation/getTaskDetail) + +### tasks:write + +Update or trash tasks + +**Associated APIs:** + +- [Create a new task](/docs/api/rest/reference/Tasks/methods/#operation/createTask) +- [Update task fields](/docs/api/rest/reference/Tasks/methods/#operation/updateTask) + +### tasks:write:admin + +Management the account's task + +**Associated APIs:** + +- [Update task fields](/docs/api/rest/reference/Tasks/methods/#operation/updateTask) + +### tasks_assignee:read + +View task's assignee(s) + +**Associated APIs:** + +- [Get assignees of a task](/docs/api/rest/reference/Tasks/methods/#operation/GetAssigneesOfATask) + +### tasks_assignee:read:admin + +View account's task assignees + +**Associated APIs:** + +- [Get assignees of a task](/docs/api/rest/reference/Tasks/methods/#operation/GetAssigneesOfATask) + +### tasks_assignee:write + +Add or remove task's assignee + +**Associated APIs:** + +- [Add assignees to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksAssignees) +- [Remove Assignee from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskAssignee) + +### tasks_assignee:write:admin + +Add or update task's assignees + +**Associated APIs:** + +- [Remove Assignee from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskAssignee) +- [Add assignees to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksAssignees) + +### tasks_collaborator:read + +View user's task collaborator information + +**Associated APIs:** + +- [Get collaborators of a task](/docs/api/rest/reference/Tasks/methods/#operation/Getcollaboratorsofatask) + +### tasks_collaborator:read:admin + +View account's task collaborators + +**Associated APIs:** + +- [Get collaborators of a task](/docs/api/rest/reference/Tasks/methods/#operation/Getcollaboratorsofatask) + +### tasks_collaborator:write + +Add or update task's collaborator + +**Associated APIs:** + +- [Remove collaborator from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskCollaborator) +- [Add collaborators to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksCollaborators) + +### tasks_collaborator:write:admin + +View and update account's task collaborator information + +**Associated APIs:** + +- [Remove collaborator from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskCollaborator) +- [Add collaborators to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksCollaborators) + +### tasks_comment:read + +Read user's task comment + +**Associated APIs:** + +- [Get a task's comments](/docs/api/rest/reference/Tasks/methods/#operation/GetAV1TasksComment) + +### tasks_comment:read:admin + +View account's task comment + +**Associated APIs:** + +- [Get a task's comments](/docs/api/rest/reference/Tasks/methods/#operation/GetAV1TasksComment) + +### tasks_comment:write + +Manage and view the user's task comments + +**Associated APIs:** + +- [Add a comment to task](/docs/api/rest/reference/Tasks/methods/#operation/addComment) +- [Delete a task's comment](/docs/api/rest/reference/Tasks/methods/#operation/DeleteTaskComment) + +### tasks_comment:write:admin + +View and manage account's task comment. + +**Associated APIs:** + +- [Delete a task's comment](/docs/api/rest/reference/Tasks/methods/#operation/DeleteTaskComment) +- [Add a comment to task](/docs/api/rest/reference/Tasks/methods/#operation/addComment) + +## TrackingField + +### tracking_fields:master + +View and manage sub account user tracking fields + +**Associated APIs:** + +- [Update a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldUpdate) +- [Get a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldGet) +- [Create a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldCreate) +- [List tracking fields](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldList) +- [Delete a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldDelete) + +### tracking_fields:read:admin + +View all users' tracking fields + +**Associated APIs:** + +- [List tracking fields](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldList) +- [Get a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldGet) + +### tracking_fields:write:admin + +View and manage all users' tracking fields + +**Associated APIs:** + +- [Create a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldCreate) +- [Update a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldUpdate) +- [Delete a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldDelete) + +## User + +### user:master + +View and manage sub account's user information + +**Associated APIs:** + +- [Update user settings](/docs/api/rest/reference/user/ma/#operation/userSettingsUpdate) +- [Delete user assistants](/docs/api/rest/reference/user/ma/#operation/userAssistantsDelete) +- [Get user permissions](/docs/api/rest/reference/user/ma/#operation/userPermission) +- [Update a user](/docs/api/rest/reference/user/ma/#operation/userUpdate) +- [Get a user](/docs/api/rest/reference/user/ma/#operation/user) +- [Revoke a user's SSO token](/docs/api/rest/reference/user/ma/#operation/userSSOTokenDelete) +- [Delete a user assistant](/docs/api/rest/reference/user/ma/#operation/userAssistantDelete) +- [Get user summary](/docs/api/rest/reference/user/ma/#operation/userSummary) +- [List users](/docs/api/rest/reference/user/ma/#operation/users) +- [Update a user's email](/docs/api/rest/reference/user/ma/#operation/userEmailUpdate) +- [Delete a user](/docs/api/rest/reference/user/ma/#operation/userDelete) +- [Delete a user's profile picture](/docs/api/rest/reference/user/ma/#operation/userPictureDelete) +- [List user schedulers](/docs/api/rest/reference/user/ma/#operation/userSchedulers) +- [Get user settings](/docs/api/rest/reference/user/ma/#operation/userSettings) +- [Delete a scheduler](/docs/api/rest/reference/user/ma/#operation/userSchedulerDelete) +- [Upload Virtual Background files](/docs/api/rest/reference/user/ma/#operation/uploadVBuser) +- [Add assistants](/docs/api/rest/reference/user/ma/#operation/userAssistantCreate) +- [Bulk update features for users](/docs/api/rest/reference/user/ma/#operation/bulkUpdateFeature) +- [Upload a user's profile picture](/docs/api/rest/reference/user/ma/#operation/userPicture) +- [Delete user schedulers](/docs/api/rest/reference/user/ma/#operation/userSchedulersDelete) +- [Get a user's token](/docs/api/rest/reference/user/ma/#operation/userToken) +- [Update user status](/docs/api/rest/reference/user/ma/#operation/userStatus) +- [Delete Virtual Background files](/docs/api/rest/reference/user/ma/#operation/delUserVB) +- [Create users](/docs/api/rest/reference/user/ma/#operation/userCreate) +- [Update a user's password](/docs/api/rest/reference/user/ma/#operation/userPassword) +- [List user assistants](/docs/api/rest/reference/user/ma/#operation/userAssistants) +- [Switch a user's account](/docs/api/rest/reference/user/ma/#operation/SwitchAUser'sAccount) + +### user:read + +View your user information + +**Associated APIs:** + +- [Check a user's PM room](/docs/api/rest/reference/user/methods/#operation/userVanityName) +- [Get user summary](/docs/api/rest/reference/user/methods/#operation/userSummary) +- [List users](/docs/api/rest/reference/user/methods/#operation/users) +- [Get a user presence status](/docs/api/rest/reference/user/methods/#operation/getUserPresenceStatus) +- [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) +- [List a user's collaboration devices](/docs/api/rest/reference/user/methods/#operation/listCollaborationDevices) +- [Get meeting summary templates](/docs/api/rest/reference/user/methods/#operation/Getmeetingsummarytemplates) +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) +- [Check a user email](/docs/api/rest/reference/user/methods/#operation/userEmail) +- [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) +- [Get user settings](/docs/api/rest/reference/user/methods/#operation/userSettings) +- [Get collaboration device detail](/docs/api/rest/reference/user/methods/#operation/getCollaborationDevice) +- [Get Meeting Template detail](/docs/api/rest/reference/user/methods/#operation/getUserMeetingTemplates) +- [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) +- [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) + +### user:read:admin + +View all user information + +**Associated APIs:** + +- [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) +- [Get a user presence status](/docs/api/rest/reference/user/methods/#operation/getUserPresenceStatus) +- [List users](/docs/api/rest/reference/user/methods/#operation/users) +- [Get user settings](/docs/api/rest/reference/user/methods/#operation/userSettings) +- [Get collaboration device detail](/docs/api/rest/reference/user/methods/#operation/getCollaborationDevice) +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) +- [List a user's collaboration devices](/docs/api/rest/reference/user/methods/#operation/listCollaborationDevices) +- [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) +- [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) +- [Get user summary](/docs/api/rest/reference/user/methods/#operation/userSummary) +- [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) +- [Get Meeting Template detail](/docs/api/rest/reference/user/methods/#operation/getUserMeetingTemplates) +- [Check a user's PM room](/docs/api/rest/reference/user/methods/#operation/userVanityName) +- [Get meeting summary templates](/docs/api/rest/reference/user/methods/#operation/Getmeetingsummarytemplates) +- [Check a user email](/docs/api/rest/reference/user/methods/#operation/userEmail) + +### user:write + +View and manage your user information + +**Associated APIs:** + +- [Update a user's presence status](/docs/api/rest/reference/user/methods/#operation/updatePresenceStatus) +- [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) +- [Add assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantCreate) +- [Get collaboration device detail](/docs/api/rest/reference/user/methods/#operation/getCollaborationDevice) +- [Delete a user assistant](/docs/api/rest/reference/user/methods/#operation/userAssistantDelete) +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) +- [Delete user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantsDelete) +- [Update a user's password](/docs/api/rest/reference/user/methods/#operation/userPassword) +- [Check a user's PM room](/docs/api/rest/reference/user/methods/#operation/userVanityName) +- [Create users](/docs/api/rest/reference/user/methods/#operation/userCreate) +- [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) +- [Bulk update features for users](/docs/api/rest/reference/user/methods/#operation/bulkUpdateFeature) +- [Delete Virtual Background files](/docs/api/rest/reference/user/methods/#operation/delUserVB) +- [Delete user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulersDelete) +- [List users](/docs/api/rest/reference/user/methods/#operation/users) +- [Get user summary](/docs/api/rest/reference/user/methods/#operation/userSummary) +- [Update user status](/docs/api/rest/reference/user/methods/#operation/userStatus) +- [List a user's collaboration devices](/docs/api/rest/reference/user/methods/#operation/listCollaborationDevices) +- [Upload a user's profile picture](/docs/api/rest/reference/user/methods/#operation/userPicture) +- [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) +- [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) +- [Update user settings](/docs/api/rest/reference/user/methods/#operation/userSettingsUpdate) +- [Delete a user's profile picture](/docs/api/rest/reference/user/methods/#operation/userPictureDelete) +- [Check a user email](/docs/api/rest/reference/user/methods/#operation/userEmail) +- [Upload Virtual Background files](/docs/api/rest/reference/user/methods/#operation/uploadVBuser) +- [Delete a user](/docs/api/rest/reference/user/methods/#operation/userDelete) +- [Delete a scheduler](/docs/api/rest/reference/user/methods/#operation/userSchedulerDelete) +- [Get user settings](/docs/api/rest/reference/user/methods/#operation/userSettings) +- [Update a user](/docs/api/rest/reference/user/methods/#operation/userUpdate) +- [Revoke a user's SSO token](/docs/api/rest/reference/user/methods/#operation/userSSOTokenDelete) +- [Update a user's email](/docs/api/rest/reference/user/methods/#operation/userEmailUpdate) + +### user:write:admin + +View users information and manage users + +**Associated APIs:** + +- [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) +- [Update a user's presence status](/docs/api/rest/reference/user/methods/#operation/updatePresenceStatus) +- [Delete a user's profile picture](/docs/api/rest/reference/user/methods/#operation/userPictureDelete) +- [Check a user email](/docs/api/rest/reference/user/methods/#operation/userEmail) +- [Create users](/docs/api/rest/reference/user/methods/#operation/userCreate) +- [Delete Virtual Background files](/docs/api/rest/reference/user/methods/#operation/delUserVB) +- [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) +- [Delete a user](/docs/api/rest/reference/user/methods/#operation/userDelete) +- [Delete a user assistant](/docs/api/rest/reference/user/methods/#operation/userAssistantDelete) +- [Check a user's PM room](/docs/api/rest/reference/user/methods/#operation/userVanityName) +- [Get user settings](/docs/api/rest/reference/user/methods/#operation/userSettings) +- [Delete user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulersDelete) +- [Get user summary](/docs/api/rest/reference/user/methods/#operation/userSummary) +- [Update user status](/docs/api/rest/reference/user/methods/#operation/userStatus) +- [Update a user's email](/docs/api/rest/reference/user/methods/#operation/userEmailUpdate) +- [Update a user's password](/docs/api/rest/reference/user/methods/#operation/userPassword) +- [Bulk update features for users](/docs/api/rest/reference/user/methods/#operation/bulkUpdateFeature) +- [Revoke a user's SSO token](/docs/api/rest/reference/user/methods/#operation/userSSOTokenDelete) +- [Upload Virtual Background files](/docs/api/rest/reference/user/methods/#operation/uploadVBuser) +- [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) +- [Update a user](/docs/api/rest/reference/user/methods/#operation/userUpdate) +- [Get collaboration device detail](/docs/api/rest/reference/user/methods/#operation/getCollaborationDevice) +- [Update user settings](/docs/api/rest/reference/user/methods/#operation/userSettingsUpdate) +- [Delete a scheduler](/docs/api/rest/reference/user/methods/#operation/userSchedulerDelete) +- [Delete user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantsDelete) +- [Add assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantCreate) +- [List a user's collaboration devices](/docs/api/rest/reference/user/methods/#operation/listCollaborationDevices) +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) +- [List users](/docs/api/rest/reference/user/methods/#operation/users) +- [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) +- [Upload a user's profile picture](/docs/api/rest/reference/user/methods/#operation/userPicture) + +### user_info:read + +View user info + +**Associated APIs:** + +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) + +### user_profile + +View your profile information + +**Associated APIs:** + +- [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) +- [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) +- [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) +- [Get a user](/docs/api/rest/reference/user/methods/#operation/user) +- [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) + +### user_zak:read + +View user’s zak token + +**Associated APIs:** + +- [Get the user's ZAK](/docs/api/rest/reference/user/methods/#operation/userZak) + +## Visitor Management + +### visitor_management:read + +View visitor management information + +**Associated APIs:** + +- [Get a list of visitors by location](/docs/api/rest/reference/zoom-rooms/methods/#operation/invitationList) +- [Invitation details by invitationID](/docs/api/rest/reference/zoom-rooms/methods/#operation/getInvitation) + +### visitor_management:write + +Update visitor management information + +**Associated APIs:** + +- [Delete an Invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteInvitation) +- [Update an invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateInvitation) +- [Check in a visitor](/docs/api/rest/reference/zoom-rooms/methods/#operation/checkinVisitor) + +### visitor_management:write:admin + +Update visitor management information + +**Associated APIs:** + +- [Send an invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/createInvitation) + +## Webinar + +### webinar:master + +View and manage sub account's user webinars + +**Associated APIs:** + +- [Get webinar tracking sources](/docs/api/rest/reference/zoom-api/ma/#operation/getTrackingSources) +- [Delete a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyDelete) +- [List past webinar instances](/docs/api/rest/reference/zoom-api/ma/#operation/pastWebinars) +- [Remove all panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistsDelete) +- [Get a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollGet) +- [Update a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/updateWebinarBrandingNameTag) +- [Delete a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingWallpaper) +- [Set webinar's default branding virtual background](/docs/api/rest/reference/zoom-api/ma/#operation/setWebinarBrandingVB) +- [Delete a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarRegistrant) +- [Get a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinar) +- [List webinar templates](/docs/api/rest/reference/zoom-api/ma/#operation/listWebinarTemplates) +- [Update a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarUpdate) +- [Delete a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollDelete) +- [Get webinar's session branding](/docs/api/rest/reference/zoom-api/ma/#operation/getWebinarBranding) +- [Upload a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/ma/#operation/uploadWebinarBrandingWallpaper) +- [Delete a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarDelete) +- [Get webinar's token](/docs/api/rest/reference/zoom-api/ma/#operation/webinarToken) +- [Update a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyUpdate) +- [List webinars](/docs/api/rest/reference/zoom-api/ma/#operation/webinars) +- [Add a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantCreate) +- [Update registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantStatus) +- [Update a live stream](/docs/api/rest/reference/zoom-api/ma/#operation/webinarLiveStreamUpdate) +- [List a webinar's polls](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPolls) +- [Delete a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingNameTag) +- [Get a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyGet) +- [Create a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarCreate) +- [List webinar registrants](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrants) +- [Upload a webinar's branding virtual background](/docs/api/rest/reference/zoom-api/ma/#operation/uploadWebinarBrandingVB) +- [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantQuestionUpdate) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/ma/#operation/addBatchWebinarRegistrants) +- [Get a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantGet) +- [Add panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistCreate) +- [Remove a panelist](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistDelete) +- [Update a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollUpdate) +- [Create a webinar template](/docs/api/rest/reference/zoom-api/ma/#operation/webinarTemplateCreate) +- [List registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantsQuestionsGet) +- [Create a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/createWebinarBrandingNameTag) +- [Create webinar's invite links](/docs/api/rest/reference/zoom-api/ma/#operation/webinarInviteLinksCreate) +- [Update live stream status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarLiveStreamStatusUpdate) +- [Get webinar absentees](/docs/api/rest/reference/zoom-api/ma/#operation/webinarAbsentees) +- [List panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelists) +- [Create a webinar's poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollCreate) +- [Delete a webinar's branding virtual backgrounds](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingVB) +- [Get live stream details](/docs/api/rest/reference/zoom-api/ma/#operation/getWebinarLiveStreamDetails) +- [Update webinar status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarStatus) + +### webinar:read + +View your webinars + +**Associated APIs:** + +- [Get webinar's token](/docs/api/rest/reference/zoom-api/methods/#operation/webinarToken) +- [Get a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantGet) +- [List webinars](/docs/api/rest/reference/zoom-api/methods/#operation/webinars) +- [Get a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollGet) +- [Get a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinar) +- [Get webinar's session branding](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarBranding) +- [Get a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyGet) +- [Get webinar tracking sources](/docs/api/rest/reference/zoom-api/methods/#operation/getTrackingSources) +- [Get live stream details](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarLiveStreamDetails) +- [List webinar registrants](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrants) +- [List a webinar's polls](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPolls) +- [List webinar templates](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarTemplates) +- [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantsQuestionsGet) +- [List past webinar poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarPollResults) +- [List webinar participants](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarParticipants) +- [List past webinar instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastWebinars) +- [List panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelists) +- [Get webinar absentees](/docs/api/rest/reference/zoom-api/methods/#operation/webinarAbsentees) +- [List Q&As of a past webinar](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarQA) + +### webinar:read:admin + +View all user Webinars + +**Associated APIs:** + +- [Get webinar's session branding](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarBranding) +- [List panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelists) +- [List Q&As of a past webinar](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarQA) +- [Get webinar absentees](/docs/api/rest/reference/zoom-api/methods/#operation/webinarAbsentees) +- [List webinar participants](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarParticipants) +- [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantsQuestionsGet) +- [List webinar templates](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarTemplates) +- [List past webinar instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastWebinars) +- [List a webinar's polls](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPolls) +- [Get a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyGet) +- [List webinar registrants](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrants) +- [Get a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollGet) +- [Get a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantGet) +- [Get a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinar) +- [Get webinar tracking sources](/docs/api/rest/reference/zoom-api/methods/#operation/getTrackingSources) +- [List webinars](/docs/api/rest/reference/zoom-api/methods/#operation/webinars) +- [List past webinar poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarPollResults) +- [Get live stream details](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarLiveStreamDetails) +- [Get webinar's token](/docs/api/rest/reference/zoom-api/methods/#operation/webinarToken) + +### webinar:write + +View and manage your webinars + +**Associated APIs:** + +- [Delete a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingWallpaper) +- [Update a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/updateWebinarBrandingNameTag) +- [Set webinar's default branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/setWebinarBrandingVB) +- [Update a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollUpdate) +- [Create a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/createWebinarBrandingNameTag) +- [Add a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantCreate) +- [Upload a webinar's branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingVB) +- [Remove all panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistsDelete) +- [Delete a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingNameTag) +- [Create webinar's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/webinarInviteLinksCreate) +- [Update a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarUpdate) +- [Delete a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollDelete) +- [Delete a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyDelete) +- [Add panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistCreate) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantQuestionUpdate) +- [Delete a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarDelete) +- [Update a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyUpdate) +- [Update a live stream](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamUpdate) +- [Remove a panelist](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistDelete) +- [Create a webinar template](/docs/api/rest/reference/zoom-api/methods/#operation/webinarTemplateCreate) +- [Create a webinar's poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollCreate) +- [Create a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarCreate) +- [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantStatus) +- [Upload a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingWallpaper) +- [Update webinar status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarStatus) +- [Delete a webinar's branding virtual backgrounds](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingVB) +- [Delete a live webinar message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarChatMessageById) +- [Delete a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarRegistrant) +- [Update live stream status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamStatusUpdate) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchWebinarRegistrants) + +### webinar:write:admin + +View and manage all user Webinars + +**Associated APIs:** + +- [Remove all panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistsDelete) +- [Delete a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarRegistrant) +- [Delete a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollDelete) +- [Delete a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingWallpaper) +- [Set webinar's default branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/setWebinarBrandingVB) +- [Update live stream status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamStatusUpdate) +- [Delete a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyDelete) +- [Update a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarUpdate) +- [Delete a webinar's branding virtual backgrounds](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingVB) +- [Create a webinar's poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollCreate) +- [Update webinar status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarStatus) +- [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantStatus) +- [Upload a webinar's branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingVB) +- [Delete a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingNameTag) +- [Create a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarCreate) +- [Create a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/createWebinarBrandingNameTag) +- [Delete a live webinar message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarChatMessageById) +- [Create webinar's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/webinarInviteLinksCreate) +- [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantQuestionUpdate) +- [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchWebinarRegistrants) +- [Add panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistCreate) +- [Update a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollUpdate) +- [Remove a panelist](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistDelete) +- [Delete a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarDelete) +- [Update a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyUpdate) +- [Upload a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingWallpaper) +- [Create a webinar template](/docs/api/rest/reference/zoom-api/methods/#operation/webinarTemplateCreate) +- [Add a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantCreate) +- [Update a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/updateWebinarBrandingNameTag) +- [Update a live stream](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamUpdate) + +### webinar:write:admin:sip_dialing + +Get CRC dial string with passcode + +**Associated APIs:** + +- [Get a webinar SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarSipDialingWithPasscode) + +### webinar:write:sip_dialing + +Get CRC dial string with passcode + +**Associated APIs:** + +- [Get a webinar SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarSipDialingWithPasscode) + +### webinar_token:read:admin:live_streaming + +View live streaming webinar token information + +**Associated APIs:** + +- [Get a webinar's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamingJoinToken) + +### webinar_token:read:admin:local_archiving + +View local archiving webinar token information + +**Associated APIs:** + +- [Get a webinar's archive token for local archiving](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLocalArchivingArchiveToken) + +### webinar_token:read:admin:local_recording + +This scope allows an app to view an account's users' local recording webinar token information + +**Associated APIs:** + +- [Get a webinar's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLocalRecordingJoinToken) + +### webinar_token:read:live_streaming + +View live streaming webinar token information + +**Associated APIs:** + +- [Get a webinar's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamingJoinToken) + +### webinar_token:read:local_recording + +This scope allows an app to view a user's local recording webinar token information + +**Associated APIs:** + +- [Get a webinar's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLocalRecordingJoinToken) + +## Whiteboard + +### whiteboard:read + +Get my whiteboard(s) + +**Associated APIs:** + +- [List all whiteboards](/docs/api/rest/reference/Whiteboard/methods/#operation/ListWhiteboards) +- [Get a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/GetAWhiteboard) + +### whiteboard:read:admin + +Get account’s whiteboard(s) + +**Associated APIs:** + +- [List all whiteboards](/docs/api/rest/reference/Whiteboard/methods/#operation/ListWhiteboards) +- [Get a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/GetAWhiteboard) + +### whiteboard:write + +Update or trash my whiteboard + +**Associated APIs:** + +- [Delete a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/DeleteAWhiteboard) +- [Create a new whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/newWhiteboardCreate) + +### whiteboard:write:admin + +Update or trash my whiteboard + +**Associated APIs:** + +- [Update whiteboard basic information](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardMetadata) +- [Delete a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/DeleteAWhiteboard) + +### whiteboard_collaborator:read:admin + +View account's whiteboard collaborator information + +**Associated APIs:** + +- [Get collaborators of a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/GetAWhiteboardCollaborator) + +### whiteboard_collaborator:write:admin + +View and manage account's whiteboard collaborator information + +**Associated APIs:** + +- [Update whiteboard collaborators](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardCollaborator) +- [Share a whiteboard to new users or team chat channels.](/docs/api/rest/reference/Whiteboard/methods/#operation/AddAWhiteboardCollaborator) +- [Remove the collaborator from a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/DeleteAWhiteboardCollaborator) + +### whiteboard_content:read + +View and export whiteboard content + +**Associated APIs:** + +- [Download Whiteboards activity file](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardsactivityfile) +- [Get whiteboard export generation status](/docs/api/rest/reference/Whiteboard/methods/#operation/Getwhiteboardexportdatagenerationstatus) +- [List whiteboards sessions](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsarchivefiles) +- [Download whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardexport) +- [List whiteboard sessions activities](/docs/api/rest/reference/Whiteboard/methods/#operation/Listwhiteboardsessionsarchivedfiles) + +### whiteboard_content:read:admin + +View and export account’s whiteboard(s) content + +**Associated APIs:** + +- [List whiteboards sessions](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsarchivefiles) +- [Download Whiteboards activity file](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardsactivityfile) +- [Download whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardexport) +- [List whiteboard sessions activities](/docs/api/rest/reference/Whiteboard/methods/#operation/Listwhiteboardsessionsarchivedfiles) +- [Get whiteboard export generation status](/docs/api/rest/reference/Whiteboard/methods/#operation/Getwhiteboardexportdatagenerationstatus) + +### whiteboard_export:write + +Create my whiteboard export data + +**Associated APIs:** + +- [Create whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsexport) + +### whiteboard_export:write:admin + +Create account's whiteboard export data + +**Associated APIs:** + +- [Create whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsexport) + +### whiteboard_file:read + +View whiteboard file information + +**Associated APIs:** + +- [Download Imported Whiteboard File](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadembeddedwhiteboardfile) + +### whiteboard_file:read:admin + +View whiteboard file information + +**Associated APIs:** + +- [Download Imported Whiteboard File](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadembeddedwhiteboardfile) + +### whiteboard_file:write + +View and manage whiteboard file informanation + +**Associated APIs:** + +- [Upload file for whiteboard import](/docs/api/rest/reference/Whiteboard/methods/#operation/Uploadfileforwhiteboardimport) + +### whiteboard_file:write:admin + +View and manage account's whiteboard file information + +**Associated APIs:** + +- [Upload file for whiteboard import](/docs/api/rest/reference/Whiteboard/methods/#operation/Uploadfileforwhiteboardimport) + +### whiteboard_import:read + +View user's whiteboard import information + +**Associated APIs:** + +- [Get whiteboard import status](/docs/api/rest/reference/Whiteboard/methods/#operation/GetWhiteboardimportstatus) + +### whiteboard_import:read:admin + +View account's whiteboard import information + +**Associated APIs:** + +- [Get whiteboard import status](/docs/api/rest/reference/Whiteboard/methods/#operation/GetWhiteboardimportstatus) + +### whiteboard_import:write + +View and manage user's whiteboard import information + +**Associated APIs:** + +- [Create a new whiteboard by import](/docs/api/rest/reference/Whiteboard/methods/#operation/CreateWhiteboardImport) + +### whiteboard_import:write:admin + +view and manage account's whiteboard import information + +**Associated APIs:** + +- [Create a new whiteboard by import](/docs/api/rest/reference/Whiteboard/methods/#operation/CreateWhiteboardImport) + +### whiteboard_project:read + +View user's project information + +**Associated APIs:** + +- [Get a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getaproject) +- [List all projects](/docs/api/rest/reference/Whiteboard/methods/#operation/Listallprojects) + +### whiteboard_project:read:admin + +View account's project information + +**Associated APIs:** + +- [List all projects](/docs/api/rest/reference/Whiteboard/methods/#operation/Listallprojects) +- [Get a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getaproject) + +### whiteboard_project:write + +View and manage user's project information + +**Associated APIs:** + +- [Update project basic information](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateproject) +- [Move whiteboards to a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Movewhiteboardstoproject) +- [Create a new project](/docs/api/rest/reference/Whiteboard/methods/#operation/Createproject) +- [Remove whiteboards from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removewhiteboardsfromaproject) +- [Delete a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Deleteproject) + +### whiteboard_project:write:admin + +View and manage account's project information + +**Associated APIs:** + +- [Update project basic information](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateproject) +- [Remove whiteboards from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removewhiteboardsfromaproject) +- [Delete a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Deleteproject) +- [Create a new project](/docs/api/rest/reference/Whiteboard/methods/#operation/Createproject) +- [Move whiteboards to a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Movewhiteboardstoproject) + +### whiteboard_project_collaborator:read + +View user's project collaborator information + +**Associated APIs:** + +- [Get collaborators of a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getcollaboratorsofaproject) + +### whiteboard_project_collaborator:read:admin + +View account's project collaborator information + +**Associated APIs:** + +- [Get collaborators of a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getcollaboratorsofaproject) + +### whiteboard_project_collaborator:write + +View and update user's project collaborator information + +**Associated APIs:** + +- [Share a project to new users](/docs/api/rest/reference/Whiteboard/methods/#operation/Shareaprojecttonewusers) +- [Update project collaborators](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateprojectcollaborators) +- [Remove the collaborator from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removethecollaboratorfromaproject) + +### whiteboard_project_collaborator:write:admin + +View and update account's project collaborator information + +**Associated APIs:** + +- [Share a project to new users](/docs/api/rest/reference/Whiteboard/methods/#operation/Shareaprojecttonewusers) +- [Remove the collaborator from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removethecollaboratorfromaproject) +- [Update project collaborators](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateprojectcollaborators) + +### whiteboard_share_setting:write + +View and manage your whiteboard sharing settings + +**Associated APIs:** + +- [Update whiteboard share setting](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardShareSetting) + +### whiteboard_share_setting:write:admin + +View and manage account's whiteboard sharing settings + +**Associated APIs:** + +- [Update whiteboard share setting](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardShareSetting) + +## Workspace + +### workspace:read + +View workspace reservation information + +**Associated APIs:** + +- [Get a workspace location floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getlocationfloormap) +- [Get a location's hot desk usage](/docs/api/rest/reference/zoom-rooms/methods/#operation/getHotDeskUsage) +- [Get a workspace reservation by reservationId](/docs/api/rest/reference/zoom-rooms/methods/#operation/GETGetaworkspacereservationbyreservationID) +- [List workspace additional information with time range](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getaworkspaceadditionalenhancements) +- [Get a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getadeskassignment) +- [Get a workspace's reservations](/docs/api/rest/reference/zoom-rooms/methods/#operation/listReservations) +- [List workspaces](/docs/api/rest/reference/zoom-rooms/methods/#operation/listWorkspaces) + +### workspace:read:admin + +View workspace reservation information for the account + +**Associated APIs:** + +- [Get a user's workspace's reservations](/docs/api/rest/reference/zoom-rooms/methods/#operation/userListReservations) +- [List workspaces](/docs/api/rest/reference/zoom-rooms/methods/#operation/listWorkspaces) +- [Get Workspace Calendar Free/Busy Event](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetWorkspaceCalendarFree/BusyEvent) +- [Get a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorkspace) +- [Get a workspace QR code](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorkspaceQRCode) +- [Get a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/get_workspace_asset) +- [Get a workspace reservation by reservationId](/docs/api/rest/reference/zoom-rooms/methods/#operation/GETGetaworkspacereservationbyreservationID) +- [Get a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getadeskassignment) +- [List workspace additional information with time range](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getaworkspaceadditionalenhancements) +- [List released workspaces by timeout](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorksapceReservationReleaseInof) +- [Get a workspace's reservations](/docs/api/rest/reference/zoom-rooms/methods/#operation/listReservations) +- [Get all workspace assets](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getallworkspaceassets) +- [Get a location's hot desk usage](/docs/api/rest/reference/zoom-rooms/methods/#operation/getHotDeskUsage) +- [List workspace reservation questionnaires](/docs/api/rest/reference/zoom-rooms/methods/#operation/Listworkspacereservationquestionnaires) + +### workspace:write + +Edit workspace reservation information + +**Associated APIs:** + +- [Update workspace settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateWorkspaceSettings) +- [Delete a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteReservation) +- [Update a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateReservation) +- [Create a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/createReservation) + +### workspace:write:admin + +Edit workspace reservation information for the account + +**Associated APIs:** + +- [Delete Workspace floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteWorkspaceFloorMap) +- [Update a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateWorkspace) +- [Update a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateReservation) +- [Create a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddaWorkspaceasset) +- [Delete a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteaWorkspaceasset) +- [Delete a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteReservation) +- [Set Workspace Calendar Free/Busy Event](/docs/api/rest/reference/zoom-rooms/methods/#operation/SetCalendarFree/BusyEvent) +- [Create a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/createWorkspace) +- [Add or Update a Workspace floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddOrUpdateAWorkspaceFloorMap) +- [Edit a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/PatchWorkspaceasset) +- [Delete a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteWorkspace) +- [Delete a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadeskassignment) +- [Create a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/createReservation) +- [Check in/out of a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/reservationEvent) +- [Update workspace settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateWorkspaceSettings) +- [Set a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/setADeskAssignment) + +## Zoom AIC + +### aic_archive:read:admin + +View all user's AIC archive files + +## Zoom Account + +### data_request:read:admin + +Allows viewing and downloading data request information + +**Associated APIs:** + +- [Get download link for data access request file](/docs/api/rest/reference/account/methods/#operation/DownloadfilesfromDataRequest) +- [List data request history](/docs/api/rest/reference/account/methods/#operation/GetDataRequestsHistory) +- [List downloadable files for export data request](/docs/api/rest/reference/account/methods/#operation/GetDownloadableFilesforDataRequest) + +### data_request:write:admin + +Allows creation and modification of data requests + +**Associated APIs:** + +- [Create data (export/deletion) request](/docs/api/rest/reference/account/methods/#operation/CreateDataAccessRequest) +- [List downloadable files for export data request](/docs/api/rest/reference/account/methods/#operation/GetDownloadableFilesforDataRequest) +- [Get download link for data access request file](/docs/api/rest/reference/account/methods/#operation/DownloadfilesfromDataRequest) +- [List data request history](/docs/api/rest/reference/account/methods/#operation/GetDataRequestsHistory) +- [Cancel data deletion request](/docs/api/rest/reference/account/methods/#operation/CancelDataRequest) + +## Zoom Auto Dialer + +### dialer:read:admin + +View Dialer + +### dialer:write:admin + +Manage Dialer + +## Zoom Clips + +### clips:read:master + +view your subAccount clips info + +**Associated APIs:** + +- [List all clips](/docs/api/rest/reference/clips/ma/#operation/GetUserClips) + +## Zoom Docs + +### docs:read + +Get basic info of the file + +**Associated APIs:** + +- [Get metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/QueryFileMetadata) +- [List all children of a file](/docs/api/rest/reference/Docs/methods/#operation/ListAllChildren) + +### docs:write + +Edit basic info of a file + +**Associated APIs:** + +- [Create a new file](/docs/api/rest/reference/Docs/methods/#operation/CreateDoc) +- [Modify metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/ModifyMetadata) + +## Zoom Events + +### zoom_events_access_links:read + +View event's Access Link + +**Associated APIs:** + +- [Get event access link](/docs/api/rest/reference/event/methods/#operation/GetEventAccessLink) +- [List event access links](/docs/api/rest/reference/event/methods/#operation/getEventAccessLinks) + +### zoom_events_access_links:read:admin + +View event's Access Link + +**Associated APIs:** + +- [Get event access link](/docs/api/rest/reference/event/methods/#operation/GetEventAccessLink) +- [List event access links](/docs/api/rest/reference/event/methods/#operation/getEventAccessLinks) + +### zoom_events_access_links:write + +View and Manage Zoom Event's access links + +**Associated APIs:** + +- [Create event access link](/docs/api/rest/reference/event/methods/#operation/createEventAccessLink) +- [Update event access](/docs/api/rest/reference/event/methods/#operation/updateEventAccess) +- [Delete event access link](/docs/api/rest/reference/event/methods/#operation/deleteEventAccessLink) + +### zoom_events_access_links:write:admin + +View and Manage Zoom Event's access links + +**Associated APIs:** + +- [Create event access link](/docs/api/rest/reference/event/methods/#operation/createEventAccessLink) +- [Update event access](/docs/api/rest/reference/event/methods/#operation/updateEventAccess) +- [Delete event access link](/docs/api/rest/reference/event/methods/#operation/deleteEventAccessLink) + +### zoom_events_attendee_actions:read + +View attendee actions performed by host for an event/session + +**Associated APIs:** + +- [List session attendee actions](/docs/api/rest/reference/event/methods/#operation/ListSessionAttendeeActions) +- [List event attendee actions](/docs/api/rest/reference/event/methods/#operation/ListEventAttendeeActions) + +### zoom_events_attendee_actions:read:admin + +View attendee actions performed by host for an event/session + +**Associated APIs:** + +- [List event attendee actions](/docs/api/rest/reference/event/methods/#operation/ListEventAttendeeActions) +- [List session attendee actions](/docs/api/rest/reference/event/methods/#operation/ListSessionAttendeeActions) + +### zoom_events_attendee_actions:write + +View and manage attendee actions performed by host for an event/session + +**Associated APIs:** + +- [Update event attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateEventAttendeeActions) +- [Update session attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateSessionAttendeeActions) + +### zoom_events_attendee_actions:write:admin + +View and manage attendee actions performed by host for an event/session + +**Associated APIs:** + +- [Update session attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateSessionAttendeeActions) +- [Update event attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateEventAttendeeActions) + +### zoom_events_basic:read + +View your Zoom events + +**Associated APIs:** + +- [List events](/docs/api/rest/reference/event/methods/#operation/getEvents) +- [Get an event](/docs/api/rest/reference/event/methods/#operation/getEventInfo) + +### zoom_events_basic:read:admin + +View all events information + +**Associated APIs:** + +- [Get an event](/docs/api/rest/reference/event/methods/#operation/getEventInfo) +- [List events](/docs/api/rest/reference/event/methods/#operation/getEvents) + +### zoom_events_basic:write + +Manage your Zoom events + +**Associated APIs:** + +- [Delete an event](/docs/api/rest/reference/event/methods/#operation/deleteEvent) +- [Event actions](/docs/api/rest/reference/event/methods/#operation/EventActions) +- [Update an event](/docs/api/rest/reference/event/methods/#operation/updateEvent) +- [Create an event](/docs/api/rest/reference/event/methods/#operation/createEvent) + +### zoom_events_basic:write:admin + +View and manage events information + +**Associated APIs:** + +- [Update an event](/docs/api/rest/reference/event/methods/#operation/updateEvent) +- [Create an event](/docs/api/rest/reference/event/methods/#operation/createEvent) +- [Event actions](/docs/api/rest/reference/event/methods/#operation/EventActions) +- [Delete an event](/docs/api/rest/reference/event/methods/#operation/deleteEvent) + +### zoom_events_coeditor:write + +View and manage coeditor information + +**Associated APIs:** + +- [Add or remove event co-editors](/docs/api/rest/reference/event/methods/#operation/coeditoractions) + +### zoom_events_coeditor:write:admin + +View and manage coeditor information + +**Associated APIs:** + +- [Add or remove event co-editors](/docs/api/rest/reference/event/methods/#operation/coeditoractions) + +### zoom_events_coeditors:read + +View Zoom event coeditors information + +**Associated APIs:** + +- [List coeditors](/docs/api/rest/reference/event/methods/#operation/getCoEditors) + +### zoom_events_coeditors:read:admin + +View Zoom events coeditor information + +**Associated APIs:** + +- [List coeditors](/docs/api/rest/reference/event/methods/#operation/getCoEditors) + +### zoom_events_email:read + +View emails for an event + +**Associated APIs:** + +- [List event email types](/docs/api/rest/reference/event/methods/#operation/listEmailTypes) +- [List event emails sent status](/docs/api/rest/reference/event/methods/#operation/listEmailSentStatuses) + +### zoom_events_email:read:admin + +View emails for an event + +**Associated APIs:** + +- [List event email types](/docs/api/rest/reference/event/methods/#operation/listEmailTypes) +- [List event emails sent status](/docs/api/rest/reference/event/methods/#operation/listEmailSentStatuses) + +### zoom_events_exhibitors:read + +View exhibitor information for an event + +**Associated APIs:** + +- [Get an exhibitor](/docs/api/rest/reference/event/methods/#operation/getExhibitorInfo) +- [List sponsor tiers](/docs/api/rest/reference/event/methods/#operation/ListSponsorTiers) +- [List exhibitors](/docs/api/rest/reference/event/methods/#operation/getExhibitors) + +### zoom_events_exhibitors:read:admin + +View exhibitor information for an event + +**Associated APIs:** + +- [Get an exhibitor](/docs/api/rest/reference/event/methods/#operation/getExhibitorInfo) +- [List sponsor tiers](/docs/api/rest/reference/event/methods/#operation/ListSponsorTiers) +- [List exhibitors](/docs/api/rest/reference/event/methods/#operation/getExhibitors) + +### zoom_events_exhibitors:write + +View and manage exhibitor information for an event + +**Associated APIs:** + +- [Create an exhibitor](/docs/api/rest/reference/event/methods/#operation/createExhibitor) +- [Delete an exhibitor](/docs/api/rest/reference/event/methods/#operation/deleteExhibitor) +- [Update exhibitor for an event](/docs/api/rest/reference/event/methods/#operation/updateExhibitor) + +### zoom_events_exhibitors:write:admin + +View and manage exhibitor information for an event + +**Associated APIs:** + +- [Create an exhibitor](/docs/api/rest/reference/event/methods/#operation/createExhibitor) +- [Update exhibitor for an event](/docs/api/rest/reference/event/methods/#operation/updateExhibitor) +- [Delete an exhibitor](/docs/api/rest/reference/event/methods/#operation/deleteExhibitor) + +### zoom_events_file:write + +zoom events file upload + +**Associated APIs:** + +- [Upload events file](/docs/api/rest/reference/event/methods/#operation/uploadEventFile) +- [Upload events multipart files](/docs/api/rest/reference/event/methods/#operation/uploadMultipartEventFile) +- [Initiate and complete the multipart file upload](/docs/api/rest/reference/event/methods/#operation/initiateAndCompleteAEventMultipartUpload.) + +### zoom_events_file:write:admin + +zoom events file upload account level + +**Associated APIs:** + +- [Upload events file](/docs/api/rest/reference/event/methods/#operation/uploadEventFile) +- [Upload events multipart files](/docs/api/rest/reference/event/methods/#operation/uploadMultipartEventFile) +- [Initiate and complete the multipart file upload](/docs/api/rest/reference/event/methods/#operation/initiateAndCompleteAEventMultipartUpload.) + +### zoom_events_hub:write + +Update Zoom Events hub configuration + +**Associated APIs:** + +- [Creates a new hub host](/docs/api/rest/reference/event/methods/#operation/createHubHost) +- [Remove hub host](/docs/api/rest/reference/event/methods/#operation/deleteHubHost) + +### zoom_events_hub:write:admin + +Update Zoom Events hub information + +**Associated APIs:** + +- [Remove hub host](/docs/api/rest/reference/event/methods/#operation/deleteHubHost) +- [Creates a new hub host](/docs/api/rest/reference/event/methods/#operation/createHubHost) + +### zoom_events_hubs:read + +View all hubs information + +**Associated APIs:** + +- [List hubs](/docs/api/rest/reference/event/methods/#operation/getHubList) +- [List hub Hosts](/docs/api/rest/reference/event/methods/#operation/gethubhostList) +- [List hub videos](/docs/api/rest/reference/event/methods/#operation/ListHubVideos) + +### zoom_events_hubs:read:admin + +View all hubs information + +**Associated APIs:** + +- [List hubs](/docs/api/rest/reference/event/methods/#operation/getHubList) +- [List hub videos](/docs/api/rest/reference/event/methods/#operation/ListHubVideos) +- [List hub Hosts](/docs/api/rest/reference/event/methods/#operation/gethubhostList) + +### zoom_events_registrants:read + +View event registrants information + +**Associated APIs:** + +- [List session attendees](/docs/api/rest/reference/event/methods/#operation/getSessionAttendeeList) +- [List registrants](/docs/api/rest/reference/event/methods/#operation/getRegistrants) + +### zoom_events_registrants:read:admin + +View event registrants information + +**Associated APIs:** + +- [List registrants](/docs/api/rest/reference/event/methods/#operation/getRegistrants) +- [List session attendees](/docs/api/rest/reference/event/methods/#operation/getSessionAttendeeList) + +### zoom_events_reports:read + +View Zoom events report + +**Associated APIs:** + +- [Get session attendance report](/docs/api/rest/reference/event/methods/#operation/SessionAttendanceReport) +- [Get custom report](/docs/api/rest/reference/event/methods/#operation/getCustomEventReport) +- [Get chat transcripts report](/docs/api/rest/reference/event/methods/#operation/ChatTranscriptsReport) +- [Get event survey report](/docs/api/rest/reference/event/methods/#operation/EventSurveyReportApi) +- [Get event attendance (Live or Lobby) report](/docs/api/rest/reference/event/methods/#operation/EventAttendanceReport) +- [Get event registrations report](/docs/api/rest/reference/event/methods/#operation/EventRegistrationsReport) + +### zoom_events_reports:read:admin + +View Zoom events reports + +**Associated APIs:** + +- [Get event registrations report](/docs/api/rest/reference/event/methods/#operation/EventRegistrationsReport) +- [Get chat transcripts report](/docs/api/rest/reference/event/methods/#operation/ChatTranscriptsReport) +- [Get event survey report](/docs/api/rest/reference/event/methods/#operation/EventSurveyReportApi) +- [Get custom report](/docs/api/rest/reference/event/methods/#operation/getCustomEventReport) +- [Get event attendance (Live or Lobby) report](/docs/api/rest/reference/event/methods/#operation/EventAttendanceReport) +- [Get session attendance report](/docs/api/rest/reference/event/methods/#operation/SessionAttendanceReport) + +### zoom_events_sessions:read + +View event session information + +**Associated APIs:** + +- [List session reservations](/docs/api/rest/reference/event/methods/#operation/ListSessionReservations) +- [Get ticket session join token by Event ID and Session ID](/docs/api/rest/reference/event/methods/#operation/getSessionJoinToken) +- [List session interpreters](/docs/api/rest/reference/event/methods/#operation/getSessionInterpreterList) +- [Get the session information](/docs/api/rest/reference/event/methods/#operation/getEventSessionInfo) +- [List sessions](/docs/api/rest/reference/event/methods/#operation/getEventSessionList) +- [List session polls](/docs/api/rest/reference/event/methods/#operation/getSessionPolls) +- [Get session livestream configuration](/docs/api/rest/reference/event/methods/#operation/getSessionLivestreamConfiguration) + +### zoom_events_sessions:read:admin + +View event session information + +**Associated APIs:** + +- [Get session livestream configuration](/docs/api/rest/reference/event/methods/#operation/getSessionLivestreamConfiguration) +- [Get ticket session join token by Event ID and Session ID](/docs/api/rest/reference/event/methods/#operation/getSessionJoinToken) +- [List session interpreters](/docs/api/rest/reference/event/methods/#operation/getSessionInterpreterList) +- [List session polls](/docs/api/rest/reference/event/methods/#operation/getSessionPolls) +- [Get the session information](/docs/api/rest/reference/event/methods/#operation/getEventSessionInfo) +- [List session reservations](/docs/api/rest/reference/event/methods/#operation/ListSessionReservations) +- [List sessions](/docs/api/rest/reference/event/methods/#operation/getEventSessionList) + +### zoom_events_sessions:write + +View and manage event session information + +**Associated APIs:** + +- [Create or update session polls](/docs/api/rest/reference/event/methods/#operation/updateSessionPolls) +- [Update a session](/docs/api/rest/reference/event/methods/#operation/updateEventSession) +- [Add session reservations](/docs/api/rest/reference/event/methods/#operation/AddSessionReservations) +- [Delete session reservations](/docs/api/rest/reference/event/methods/#operation/DeleteSessionReservations) +- [Update session livestream configuration](/docs/api/rest/reference/event/methods/#operation/UpdateSessionLivestreamConfiguration) +- [Delete a session](/docs/api/rest/reference/event/methods/#operation/deleteEventSession) +- [Create a session](/docs/api/rest/reference/event/methods/#operation/createEventSession) +- [Create or update session interpreters](/docs/api/rest/reference/event/methods/#operation/updateSessionInterpreters) + +### zoom_events_sessions:write:admin + +View and manage event session information + +**Associated APIs:** + +- [Create or update session polls](/docs/api/rest/reference/event/methods/#operation/updateSessionPolls) +- [Add session reservations](/docs/api/rest/reference/event/methods/#operation/AddSessionReservations) +- [Delete a session](/docs/api/rest/reference/event/methods/#operation/deleteEventSession) +- [Create a session](/docs/api/rest/reference/event/methods/#operation/createEventSession) +- [Update session livestream configuration](/docs/api/rest/reference/event/methods/#operation/UpdateSessionLivestreamConfiguration) +- [Delete session reservations](/docs/api/rest/reference/event/methods/#operation/DeleteSessionReservations) +- [Update a session](/docs/api/rest/reference/event/methods/#operation/updateEventSession) +- [Create or update session interpreters](/docs/api/rest/reference/event/methods/#operation/updateSessionInterpreters) + +### zoom_events_speakers:read + +View event speaker information + +**Associated APIs:** + +- [List speakers](/docs/api/rest/reference/event/methods/#operation/getSpeakers) +- [Get a speaker](/docs/api/rest/reference/event/methods/#operation/getSpeaker) + +### zoom_events_speakers:read:admin + +View event speaker information + +**Associated APIs:** + +- [List speakers](/docs/api/rest/reference/event/methods/#operation/getSpeakers) +- [Get a speaker](/docs/api/rest/reference/event/methods/#operation/getSpeaker) + +### zoom_events_speakers:write + +Manage event speaker information + +**Associated APIs:** + +- [Create a speaker](/docs/api/rest/reference/event/methods/#operation/createSpeaker) +- [Update a speaker](/docs/api/rest/reference/event/methods/#operation/updateSpeaker) +- [Delete a speaker](/docs/api/rest/reference/event/methods/#operation/deleteSpeaker) + +### zoom_events_speakers:write:admin + +Manage event speaker information + +**Associated APIs:** + +- [Create a speaker](/docs/api/rest/reference/event/methods/#operation/createSpeaker) +- [Delete a speaker](/docs/api/rest/reference/event/methods/#operation/deleteSpeaker) +- [Update a speaker](/docs/api/rest/reference/event/methods/#operation/updateSpeaker) + +### zoom_events_ticket_types:read + +View all event ticket types + +**Associated APIs:** + +- [List ticket types](/docs/api/rest/reference/event/methods/#operation/getEventTicketTypes) +- [List registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForTicketType) +- [List registration questions for an event](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForEvent) + +### zoom_events_ticket_types:read:admin + +View all event ticket types + +**Associated APIs:** + +- [List ticket types](/docs/api/rest/reference/event/methods/#operation/getEventTicketTypes) +- [List registration questions for an event](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForEvent) +- [List registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForTicketType) + +### zoom_events_ticket_types:write + +View and manage event ticket types + +**Associated APIs:** + +- [Create an event ticket type](/docs/api/rest/reference/event/methods/#operation/createTicketType) +- [Update registration questions for an event](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForEvent) +- [Update ticket type for an event](/docs/api/rest/reference/event/methods/#operation/updateTicketType) +- [Delete a ticket type](/docs/api/rest/reference/event/methods/#operation/deleteEventTicketType) +- [Update registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForTicketType) + +### zoom_events_ticket_types:write:admin + +View and manage event ticket types + +**Associated APIs:** + +- [Update ticket type for an event](/docs/api/rest/reference/event/methods/#operation/updateTicketType) +- [Update registration questions for an event](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForEvent) +- [Create an event ticket type](/docs/api/rest/reference/event/methods/#operation/createTicketType) +- [Update registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForTicketType) +- [Delete a ticket type](/docs/api/rest/reference/event/methods/#operation/deleteEventTicketType) + +### zoom_events_tickets:read + +View event tickets and event ticket details + +**Associated APIs:** + +- [Get a ticket](/docs/api/rest/reference/event/methods/#operation/getTicketDetails) +- [List tickets](/docs/api/rest/reference/event/methods/#operation/getTickets) + +### zoom_events_tickets:read:admin + +View event tickets information + +**Associated APIs:** + +- [Get a ticket](/docs/api/rest/reference/event/methods/#operation/getTicketDetails) +- [List tickets](/docs/api/rest/reference/event/methods/#operation/getTickets) + +### zoom_events_tickets:write + +Create event tickets + +**Associated APIs:** + +- [Create tickets](/docs/api/rest/reference/event/methods/#operation/createTickets) +- [Delete a ticket](/docs/api/rest/reference/event/methods/#operation/deleteTicket) +- [Update ticket](/docs/api/rest/reference/event/methods/#operation/Updateticket) + +### zoom_events_tickets:write:admin + +View and manage event tickets information + +**Associated APIs:** + +- [Delete a ticket](/docs/api/rest/reference/event/methods/#operation/deleteTicket) +- [Create tickets](/docs/api/rest/reference/event/methods/#operation/createTickets) +- [Update ticket](/docs/api/rest/reference/event/methods/#operation/Updateticket) + +### zoom_events_vod_channels:read + +View on-demand video channels + +**Associated APIs:** + +- [Get VOD channel details](/docs/api/rest/reference/event/methods/#operation/getVODChannelDetail) +- [List VOD channel videos](/docs/api/rest/reference/event/methods/#operation/ListVODChannelVideos) +- [List channels](/docs/api/rest/reference/event/methods/#operation/getVODChannels) + +### zoom_events_vod_channels:read:admin + +View on-demand video channels + +**Associated APIs:** + +- [List channels](/docs/api/rest/reference/event/methods/#operation/getVODChannels) +- [List VOD channel videos](/docs/api/rest/reference/event/methods/#operation/ListVODChannelVideos) +- [Get VOD channel details](/docs/api/rest/reference/event/methods/#operation/getVODChannelDetail) + +### zoom_events_vod_channels:write + +View and manage on-demand video channels + +**Associated APIs:** + +- [Add VOD channel videos](/docs/api/rest/reference/event/methods/#operation/AddVODChannelVideos) +- [Create VOD channel](/docs/api/rest/reference/event/methods/#operation/createVodChannel) +- [Delete VOD channel video](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannelVideo) +- [VOD channel actions](/docs/api/rest/reference/event/methods/#operation/vodChannelActions) +- [Update VOD channel](/docs/api/rest/reference/event/methods/#operation/UpdateVideoChannel) +- [Delete VOD Channel](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannel) + +### zoom_events_vod_channels:write:admin + +View and manage on-demand video channels + +**Associated APIs:** + +- [Update VOD channel](/docs/api/rest/reference/event/methods/#operation/UpdateVideoChannel) +- [Add VOD channel videos](/docs/api/rest/reference/event/methods/#operation/AddVODChannelVideos) +- [Create VOD channel](/docs/api/rest/reference/event/methods/#operation/createVodChannel) +- [Delete VOD channel video](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannelVideo) +- [VOD channel actions](/docs/api/rest/reference/event/methods/#operation/vodChannelActions) +- [Delete VOD Channel](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannel) + +### zoom_events_vod_registration:read + +Read VOD channel registrations + +**Associated APIs:** + +- [List VOD Registration](/docs/api/rest/reference/event/methods/#operation/ListVODRegistration) +- [Get VOD Registration Questions](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForVODChannel) + +### zoom_events_vod_registration:read:admin + +Read VOD channel registration settings + +**Associated APIs:** + +- [List VOD Registration](/docs/api/rest/reference/event/methods/#operation/ListVODRegistration) +- [Get VOD Registration Questions](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForVODChannel) + +### zoom_events_vod_registration:write + +Update VOD channel registrations + +**Associated APIs:** + +- [update VOD channel registration questions](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForVODchannel) +- [VOD channel registration](/docs/api/rest/reference/event/methods/#operation/VODTicketRegistration) + +### zoom_events_vod_registration:write:admin + +Update VOD channel registrations + +**Associated APIs:** + +- [update VOD channel registration questions](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForVODchannel) +- [VOD channel registration](/docs/api/rest/reference/event/methods/#operation/VODTicketRegistration) + +### zoom_events_vod_reports:read + +Zoom Events VOD reports + +**Associated APIs:** + +- [Get VOD channel registration report](/docs/api/rest/reference/event/methods/#operation/VodChannelReigistration) + +### zoom_events_vod_reports:read:admin + +Zoom events VOD reports + +**Associated APIs:** + +- [Get VOD channel registration report](/docs/api/rest/reference/event/methods/#operation/VodChannelReigistration) + +## Zoom IQ + +### iq_account:read:admin + +View all account settings information + +**Associated APIs:** + +- [Get indicators settings [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/accountSettingsIndicatorsDeprecated) +- [Get indicators settings](/docs/api/rest/reference/iq/methods/#operation/accountIndicatorsSettings) + +### iq_coaching:read + +View your coaching information + +**Associated APIs:** + +- [Get conversation scorecards](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsById) +- [Get conversation scorecards [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsDeprecated) + +### iq_coaching:read:admin + +View all coaching information + +**Associated APIs:** + +- [Get conversation scorecards](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsById) +- [Get conversation scorecards [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsDeprecated) + +### iq_comment:read + +View your comments information + +**Associated APIs:** + +- [Get conversation comments](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsById) +- [Get conversation comments [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsDeprecated) + +### iq_comment:read:admin + +Manage all comments information + +**Associated APIs:** + +- [Get conversation comments](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsById) +- [Get conversation comments [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsDeprecated) + +### iq_comment:write + +Mangage your comments information + +**Associated APIs:** + +- [Get conversation comments [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsDeprecated) +- [Get conversation comments](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsById) +- [Add new comments to the conversation](/docs/api/rest/reference/iq/methods/#operation/addConversationComments) +- [Add new comments to the conversation [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationCommentDeprecated) +- [Delete conversation's comment](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentById) +- [Edit conversation comment](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentById) +- [Delete conversation's comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentDeprecated) +- [Edit conversation comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentDeprecated) + +### iq_comment:write:admin + +Manage all comments information + +**Associated APIs:** + +- [Add new comments to the conversation [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationCommentDeprecated) +- [Edit conversation comment](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentById) +- [Delete conversation's comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentDeprecated) +- [Delete conversation's comment](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentById) +- [Edit conversation comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentDeprecated) +- [Add new comments to the conversation](/docs/api/rest/reference/iq/methods/#operation/addConversationComments) +- [Get conversation comments [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsDeprecated) +- [Get conversation comments](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsById) + +### iq_conversation:read + +View your conversations information + +**Associated APIs:** + +- [Get conversation content analysis](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisById) +- [Get conversation content analysis [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisDeprecated) +- [Get conversation interactions [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInteractionsDeprecated) +- [List conversations](/docs/api/rest/reference/iq/methods/#operation/listAllConversations) +- [Get conversation information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInfoDeprecated) +- [Get conversation interactions](/docs/api/rest/reference/iq/methods/#operation/getConversationInteraction) +- [List scheduled meetings](/docs/api/rest/reference/iq/methods/#operation/Listallscheduledmeetings) +- [Get conversation information](/docs/api/rest/reference/iq/methods/#operation/getConversationDetail) +- [List conversations [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listConversationsDeprecated) + +### iq_conversation:read:admin + +View all conversations information + +**Associated APIs:** + +- [Get conversation content analysis](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisById) +- [List conversations](/docs/api/rest/reference/iq/methods/#operation/listAllConversations) +- [Get conversation interactions [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInteractionsDeprecated) +- [Get conversation content analysis [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisDeprecated) +- [Get conversation information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInfoDeprecated) +- [List conversations [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listConversationsDeprecated) +- [Get conversation interactions](/docs/api/rest/reference/iq/methods/#operation/getConversationInteraction) +- [List scheduled meetings](/docs/api/rest/reference/iq/methods/#operation/Listallscheduledmeetings) +- [Get conversation information](/docs/api/rest/reference/iq/methods/#operation/getConversationDetail) + +### iq_conversation:write + +Manage your conversations information + +**Associated APIs:** + +- [Update conversation host id to new host id by conversation id [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UpdateconversationhostidtonewhostidbyconversationidDeprecated) +- [Get conversation information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInfoDeprecated) +- [Get conversation interactions [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInteractionsDeprecated) +- [Delete conversation by conversation ID [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationDeprecated) +- [Add conversation by file id or download url.](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrl) +- [Add conversation by meeting record url or meeting UUID.](/docs/api/rest/reference/iq/methods/#operation/addConversationByRecord) +- [Upload IQ file [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIQFileDeprecated) +- [Get conversation interactions](/docs/api/rest/reference/iq/methods/#operation/getConversationInteraction) +- [List scheduled meetings](/docs/api/rest/reference/iq/methods/#operation/Listallscheduledmeetings) +- [Initiate and complete a multipart upload.](/docs/api/rest/reference/iq/methods/#operation/InitiateAndCompleteAMultipartUpload) +- [Get conversation information](/docs/api/rest/reference/iq/methods/#operation/getConversationDetail) +- [Add conversation by file id or download url. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrlDeprecated) +- [Add conversation by meeting record url or meeting UUID. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationDeprecated) +- [Upload iq multipart file.](/docs/api/rest/reference/iq/methods/#operation/UploadZraMultipartFile.) +- [Get conversation content analysis [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisDeprecated) +- [Upload IQ file](/docs/api/rest/reference/iq/methods/#operation/UploadZraFile) +- [Update conversation host id to new host id by conversation id](/docs/api/rest/reference/iq/methods/#operation/UpdateConversationhostid2NewHostidByConversationId) +- [Delete conversation by conversation ID](/docs/api/rest/reference/iq/methods/#operation/deleteConversationById) +- [Get conversation content analysis](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisById) +- [List conversations](/docs/api/rest/reference/iq/methods/#operation/listAllConversations) +- [List conversations [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listConversationsDeprecated) +- [Upload iq multipart file. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIqMultipartFileDeprecated) + +### iq_conversation:write:admin + +Manage all conversations information + +**Associated APIs:** + +- [Get conversation interactions](/docs/api/rest/reference/iq/methods/#operation/getConversationInteraction) +- [List conversations [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listConversationsDeprecated) +- [Upload iq multipart file. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIqMultipartFileDeprecated) +- [Delete conversation by conversation ID [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationDeprecated) +- [Update conversation host id to new host id by conversation id [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UpdateconversationhostidtonewhostidbyconversationidDeprecated) +- [List conversations](/docs/api/rest/reference/iq/methods/#operation/listAllConversations) +- [Upload IQ file [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIQFileDeprecated) +- [Get conversation interactions [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInteractionsDeprecated) +- [Upload iq multipart file.](/docs/api/rest/reference/iq/methods/#operation/UploadZraMultipartFile.) +- [Add conversation by meeting record url or meeting UUID. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationDeprecated) +- [List scheduled meetings](/docs/api/rest/reference/iq/methods/#operation/Listallscheduledmeetings) +- [Get conversation information](/docs/api/rest/reference/iq/methods/#operation/getConversationDetail) +- [Add conversation by meeting record url or meeting UUID.](/docs/api/rest/reference/iq/methods/#operation/addConversationByRecord) +- [Update conversation host id to new host id by conversation id](/docs/api/rest/reference/iq/methods/#operation/UpdateConversationhostid2NewHostidByConversationId) +- [Get conversation content analysis](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisById) +- [Add conversation by file id or download url. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrlDeprecated) +- [Get conversation content analysis [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisDeprecated) +- [Upload IQ file](/docs/api/rest/reference/iq/methods/#operation/UploadZraFile) +- [Initiate and complete a multipart upload.](/docs/api/rest/reference/iq/methods/#operation/InitiateAndCompleteAMultipartUpload) +- [Get conversation information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInfoDeprecated) +- [Add conversation by file id or download url.](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrl) +- [Delete conversation by conversation ID](/docs/api/rest/reference/iq/methods/#operation/deleteConversationById) + +### iq_deal:read + +Retrieve ZoomIQ Deal information with activity + +**Associated APIs:** + +- [List deals [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listDealsDeprecated) +- [Get deal information](/docs/api/rest/reference/iq/methods/#operation/getDealDetail) +- [List deals](/docs/api/rest/reference/iq/methods/#operation/listAllDeals) +- [Get deal activities](/docs/api/rest/reference/iq/methods/#operation/geAllActivitiesFromDeal) +- [Get deal information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealInfoDeprecated) +- [Get deal activities [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealActivitiesDeprecated) + +### iq_deal:read:admin + +Retrieve ZoomIQ Deal information with activity + +**Associated APIs:** + +- [Get deal information](/docs/api/rest/reference/iq/methods/#operation/getDealDetail) +- [List deals [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listDealsDeprecated) +- [Get deal information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealInfoDeprecated) +- [Get deal activities [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealActivitiesDeprecated) +- [Get deal activities](/docs/api/rest/reference/iq/methods/#operation/geAllActivitiesFromDeal) +- [List deals](/docs/api/rest/reference/iq/methods/#operation/listAllDeals) + +### iq_deal:write + +View and manage deal activities + +**Associated APIs:** + +- [List deals [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listDealsDeprecated) +- [Get deal information](/docs/api/rest/reference/iq/methods/#operation/getDealDetail) +- [Get deal activities [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealActivitiesDeprecated) +- [Delete activity from the deal](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromDeal) +- [Get deal information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealInfoDeprecated) +- [Get deal activities](/docs/api/rest/reference/iq/methods/#operation/geAllActivitiesFromDeal) +- [Delete activity from the deal [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromTheDealDeprecated) +- [List deals](/docs/api/rest/reference/iq/methods/#operation/listAllDeals) + +### iq_deal:write:admin + +View and manage deal activities + +**Associated APIs:** + +- [Delete activity from the deal [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromTheDealDeprecated) +- [Get deal activities [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealActivitiesDeprecated) +- [Delete activity from the deal](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromDeal) +- [List deals [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listDealsDeprecated) +- [Get deal information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealInfoDeprecated) +- [Get deal information](/docs/api/rest/reference/iq/methods/#operation/getDealDetail) +- [List deals](/docs/api/rest/reference/iq/methods/#operation/listAllDeals) +- [Get deal activities](/docs/api/rest/reference/iq/methods/#operation/geAllActivitiesFromDeal) + +### iq_playlist:read + +View all playlists information + +**Associated APIs:** + +- [Get a user's playlist [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylistDeprecated) +- [Get a user's playlist](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylists) + +### iq_playlist:read:admin + +View all playlists information + +**Associated APIs:** + +- [Get a user's playlist [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylistDeprecated) +- [Get a user's playlist](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylists) + +### iq_team:read + +Get team info in User level + +**Associated APIs:** + +- [List Account Teams](/docs/api/rest/reference/iq/methods/#operation/ListTeams) +- [List Account Teams [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/ListAccountTeamsDeprecated) + +### iq_team:read:admin + +Get team info + +**Associated APIs:** + +- [List Account Teams [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/ListAccountTeamsDeprecated) +- [List Unassigned Team Users](/docs/api/rest/reference/iq/methods/#operation/ListUnassignedTeamUsers) +- [Get Team Detail](/docs/api/rest/reference/iq/methods/#operation/GetTeamDetail) +- [List Team Managers](/docs/api/rest/reference/iq/methods/#operation/ListTeamManagers) +- [List Account Teams](/docs/api/rest/reference/iq/methods/#operation/ListTeams) +- [List Team Members](/docs/api/rest/reference/iq/methods/#operation/ListTeamMembers) + +### iq_team:write:admin + +team resource read&edit + +**Associated APIs:** + +- [Remove additional access from current team](/docs/api/rest/reference/iq/methods/#operation/DeleteSharedFromTeams) +- [Assign Team Members](/docs/api/rest/reference/iq/methods/#operation/AssignTeamMembers) +- [Move team to new parent](/docs/api/rest/reference/iq/methods/#operation/MoveTeam) +- [Assign Team Managers](/docs/api/rest/reference/iq/methods/#operation/AssignTeamManagers) +- [Unassign Team Managers](/docs/api/rest/reference/iq/methods/#operation/UnassignTeamManagers) +- [Create Team](/docs/api/rest/reference/iq/methods/#operation/CreateTeam) +- [Delete Team](/docs/api/rest/reference/iq/methods/#operation/DeleteTeam) +- [Update Team name](/docs/api/rest/reference/iq/methods/#operation/UpdateTeam) +- [Remove additional access from target teams](/docs/api/rest/reference/iq/methods/#operation/DeleteSharedToTeams) +- [Grant additional access to current team](/docs/api/rest/reference/iq/methods/#operation/AddSharedFromTeams) +- [Unassign Team Members](/docs/api/rest/reference/iq/methods/#operation/UnassignTeamMembers) +- [Grant additional access to target teams](/docs/api/rest/reference/iq/methods/#operation/AddSharedToTeams) + +## Zoom Meeting + +### meeting_summary:write + +View and manage your meeting summaries + +**Associated APIs:** + +- [Delete a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Deletemeetingorwebinarsummary) + +### meeting_summary:write:admin + +View and manage all user meeting summaries + +**Associated APIs:** + +- [Delete a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Deletemeetingorwebinarsummary) + +## Zoom Quality Management + +### qm_interactions:write + +Manage the interactions. + +### qm_interactions:write:admin + +Manage the interactions. + +## Zoom User + +### division:read:admin + +View divisions + +**Associated APIs:** + +- [List divisions](/docs/api/rest/reference/user/methods/#operation/listDivisions) +- [Get a division](/docs/api/rest/reference/user/methods/#operation/Getdivision) +- [List division members](/docs/api/rest/reference/user/methods/#operation/listDivisionMembers) + +### division:wirte:admin + +View and manage divisions + +**Associated APIs:** + +- [Create a division](/docs/api/rest/reference/user/methods/#operation/Createadivision) +- [Update a division](/docs/api/rest/reference/user/methods/#operation/Updateadivision) +- [Get a division](/docs/api/rest/reference/user/methods/#operation/Getdivision) +- [Assign a division](/docs/api/rest/reference/user/methods/#operation/assigndivisionMember) +- [Delete a division](/docs/api/rest/reference/user/methods/#operation/Deletedivision) + +## Zoom Video Management + +### video_mgmt_channels:read + +View video channels information + +**Associated APIs:** + +- [List channels](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listVideoChannels) +- [List channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listChannelPermissions) +- [List channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelVideos) +- [List channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelPlaylists) +- [Get channel details](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/getChannelDetail) + +### video_mgmt_channels:read:admin + +View video channels information + +**Associated APIs:** + +- [List channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listChannelPermissions) +- [Get channel details](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/getChannelDetail) +- [List channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelPlaylists) +- [List channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelVideos) +- [List channels](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listVideoChannels) + +### video_mgmt_channels:write + +Manage video channels + +**Associated APIs:** + +- [Add channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelPlaylists) +- [Delete channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannel) +- [Channel actions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/channelActions) +- [Update channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdateVideoChannel) +- [Create a channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannel) +- [Delete channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelVideos) +- [Create channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannelPermissions) +- [Delete channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPermissions) +- [Add channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelVideos) +- [Delete channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPlaylists) +- [Update channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/updateChannelPermissions) + +### video_mgmt_channels:write:admin + +Manage video channels + +**Associated APIs:** + +- [Add channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelPlaylists) +- [Channel actions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/channelActions) +- [Update channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdateVideoChannel) +- [Delete channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPermissions) +- [Add channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelVideos) +- [Delete channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelVideos) +- [Update channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/updateChannelPermissions) +- [Create a channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannel) +- [Create channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannelPermissions) +- [Delete channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPlaylists) +- [Delete channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannel) + +### video_mgmt_file:write + +Upload file to video management + +**Associated APIs:** + +- [Upload file for video management](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/uploadVODtFile) + +### video_mgmt_file:write:admin + +upload a file to video management + +**Associated APIs:** + +- [Upload file for video management](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/uploadVODtFile) + +### video_mgmt_playlists:read + +View playlists + +**Associated APIs:** + +- [List playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylistVideos) +- [List playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylists) + +### video_mgmt_playlists:read:admin + +View playlists + +**Associated APIs:** + +- [List playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylists) +- [List playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylistVideos) + +### video_mgmt_playlists:write + +Manage playlists + +**Associated APIs:** + +- [Delete playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylistVideos) +- [Delete playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylist) +- [Create a playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createPlaylist) +- [Update playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdatePlaylist) +- [Add playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddPlaylistVideos) + +### video_mgmt_playlists:write:admin + +Manage playlists + +**Associated APIs:** + +- [Create a playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createPlaylist) +- [Delete playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylist) +- [Update playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdatePlaylist) +- [Delete playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylistVideos) +- [Add playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddPlaylistVideos) + +### video_mgmt_videos:read + +View videos information + +**Associated APIs:** + +- [List all videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListAllVideos) + +### video_mgmt_videos:read:admin + +View videos information + +**Associated APIs:** + +- [List all videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListAllVideos) + +## ZoomCommerce + +### zoom_commerce:read:admin + +Zoom commerce API read access for account admin + +**Associated APIs:** + +- [Gets all quotes for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/getAllQuotes) +- [Gets all valid Deal Registrations for a partner](/docs/api/rest/reference/commerce/methods/#operation/getAllDealRegs) +- [Get the list of all accounts associated with a Zoom Partner/Sub-Reseller, by the account type](/docs/api/rest/reference/commerce/methods/#operation/getAllAccounts) +- [Gets details of all files associated with a quote or deal registration](/docs/api/rest/reference/commerce/methods/#operation/allFileDetails) +- [Get detailed information about a specific invoice for a distributor or a reseller](/docs/api/rest/reference/commerce/methods/#operation/getInvoiceDetail) +- [Gets the PDF document for the billing document ID](/docs/api/rest/reference/commerce/methods/#operation/downloadBillingDoc) +- [Gets details of a deal registration by the deal registration](/docs/api/rest/reference/commerce/methods/#operation/getDealRegDetails) +- [Retrieves all valid Zoom Campaigns which a deal registration can be associated with.](/docs/api/rest/reference/commerce/methods/#operation/getCampaigns) +- [Gets the details for a Zoom product or offer.](/docs/api/rest/reference/commerce/methods/#operation/getOfferDetail) +- [Gets all orders for a Zoom partner.](/docs/api/rest/reference/commerce/methods/#operation/getAllOrders) +- [Download a file associated with a quote or deal registration.](/docs/api/rest/reference/commerce/methods/#operation/downloadFile.) +- [Gets Zoom Product Catalog for a Zoom Partner](/docs/api/rest/reference/commerce/methods/#operation/getOffers) +- [Get trial details for an end customer by their Zoom account number or the trial ID](/docs/api/rest/reference/commerce/methods/#operation/getTrialDetails) +- [Gets subscription details for a given subscription number](/docs/api/rest/reference/commerce/methods/#operation/getSubscriptionDetails) +- [Gets all billing documents for a distributor or a reseller](/docs/api/rest/reference/commerce/methods/#operation/getAllBillingDocs) +- [Get order details by order reference ID](/docs/api/rest/reference/commerce/methods/#operation/getOrderDetails) +- [Get quote details by quote reference ID](/docs/api/rest/reference/commerce/methods/#operation/getQuoteDetails) +- [Gets subscription changes/versions for a given subscription number.](/docs/api/rest/reference/commerce/methods/#operation/getSubscriptionVersions) +- [Get trial subscriptions for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/getAllTrialSubscriptions) +- [Get the account details for a Zoom Partner/Subreseller/End Customer](/docs/api/rest/reference/commerce/methods/#operation/getAccountDetails) +- [Gets the pricebook in a downloadable file](/docs/api/rest/reference/commerce/methods/#operation/downloadPricebook) +- [Gets paid subscriptions for a Zoom partner.](/docs/api/rest/reference/commerce/methods/#operation/getAllSubscriptions) + +### zoom_commerce:write:admin + +Zoom Commerce API write access for account admin + +**Associated APIs:** + +- [Create an end customer account](/docs/api/rest/reference/commerce/methods/#operation/createAccount) +- [Update a subscription quote for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/updateQuote) +- [Preview delta order metrics and subscriptions in an order](/docs/api/rest/reference/commerce/methods/#operation/createOrderPreview) +- [Upload an attachment pdf file in context of a deal registration or quote](/docs/api/rest/reference/commerce/methods/#operation/uploadFile) +- [Updates an existing deal registration](/docs/api/rest/reference/commerce/methods/#operation/Updatesanexistingdealregistration) +- [Create a subscription order for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/createOrder) +- [Preview delta quote metrics and subscriptions in a quote](/docs/api/rest/reference/commerce/methods/#operation/createQuotePreview) +- [Add contacts to an existing end customer or your own account.](/docs/api/rest/reference/commerce/methods/#operation/addAccountContact) +- [Creates a new deal registration for a partner](/docs/api/rest/reference/commerce/methods/#operation/createDealReg) +- [Create a subscription quote for a Zoom Partner](/docs/api/rest/reference/commerce/methods/#operation/createQuote) +- [Submits a subscription quote for provisioning](/docs/api/rest/reference/commerce/methods/#operation/provisionQuote) + +## ZoomVirtualAgent + +### zva:read:km_kbs + +View your knowledge bases and articles + +**Associated APIs:** + +- [Get article](/docs/api/rest/reference/virtual-agent/methods/#operation/GetArticle) +- [Get articles](/docs/api/rest/reference/virtual-agent/methods/#operation/GetArticles) +- [Get sync](/docs/api/rest/reference/virtual-agent/methods/#operation/GetSync) + +### zva:write:km_kbs + +View and manage your knowledge bases and articles + +**Associated APIs:** + +- [Create sync request](/docs/api/rest/reference/virtual-agent/methods/#operation/CreateSyncRequest) +- [Create article](/docs/api/rest/reference/virtual-agent/methods/#operation/CreateArticle) +- [Delete article](/docs/api/rest/reference/virtual-agent/methods/#operation/DeleteArticle) +- [Update article](/docs/api/rest/reference/virtual-agent/methods/#operation/UpdateArticle) + +### zva_report:read + +View zva engagements + +**Associated APIs:** + +- [Get ZVA query details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAQueryDetails) +- [Get ZVA transcripts](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVATranscripts) +- [Get ZVA variable details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAengagementvariabledetails) +- [Get ZVA engagements](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAEngagements) +- [Get ZVA Surveys](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVASurveys) + +## workforce_management + +### workforce_management:read:admin + +View workforce management information + +**Associated APIs:** + +- [Next Migrate scopes](/docs/integrations/migrate/) +- [GitHub](https://github.com/zoom) +- [Youtube](https://www.youtube.com/@ZoomDevelopers) +- [Developer Forum](https://devforum.zoom.us/) +- [Help](/support/) +- [Terms](https://zoom.us/terms) +- [Privacy Policy](https://zoom.us/privacy) +- [API Terms of Use](https://explore.zoom.us/docs/en-us/zoom_api_license_and_tou.html) +- [Marketplace Developer Agreement](https://explore.zoom.us/en/marketplace-developer-agreement/) + diff --git a/partner-built/zoom-plugin/skills/oauth/references/environment-variables.md b/partner-built/zoom-plugin/skills/oauth/references/environment-variables.md new file mode 100644 index 00000000..f678dbbf --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/references/environment-variables.md @@ -0,0 +1,23 @@ +# Zoom OAuth Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth client identity | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth client secret | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | User-level OAuth | Authorization code callback | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_ACCOUNT_ID` | S2S OAuth | Account-level token grant | Zoom Marketplace -> Server-to-Server OAuth app credentials | + +## Runtime-only values + +- `ZOOM_AUTH_CODE` +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +Generate these at runtime and keep in secure storage. + +## Notes + +- Use S2S OAuth where user consent is not required. +- Use Authorization Code flow when acting on behalf of a Zoom user. diff --git a/partner-built/zoom-plugin/skills/oauth/references/granular-scopes.md b/partner-built/zoom-plugin/skills/oauth/references/granular-scopes.md new file mode 100644 index 00000000..05be7472 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/references/granular-scopes.md @@ -0,0 +1,3458 @@ +# Granular OAuth Scopes + +> Source: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + + +## Conference Room Connector (CRC) + + +### Account + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/methods/#operation/getCiscoPolycomRoomAccountSetting) | `crc:read:rooms_account_settings:admin` | +| [Get Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/ma/#operation/getCiscoPolycomRoomAccountSetting) | `crc:read:rooms_account_settings:master` | +| [Update Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/methods/#operation/UpdateCiscoPolycomRoomAccountSetting) | `crc:update:rooms_account_settings:admin` | +| [Update Cisco/Polycom Room Account Setting](/docs/api/rest/reference/crc/ma/#operation/UpdateCiscoPolycomRoomAccountSetting) | `crc:update:rooms_account_settings:master` | + +### Api Connector + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get an API Connector's private key](/docs/api/rest/reference/crc/methods/#operation/GetanAPIConnector'sprivatekey) | `crc:read:apiconnector_private_key:admin` | +| [Get an API Connector's private key](/docs/api/rest/reference/crc/ma/#operation/GetanAPIConnector'sprivatekey) | `crc:read:apiconnector_private_key:master` | +| [Get an API Connector](/docs/api/rest/reference/crc/methods/#operation/GetAPIConnector) | `crc:read:apiconnector:admin` | +| [Get an API Connector](/docs/api/rest/reference/crc/ma/#operation/GetAPIConnector) | `crc:read:apiconnector:master` | +| [List API Connectors](/docs/api/rest/reference/crc/methods/#operation/GetListAPIConnectors) | `crc:read:list_apiconnectors:admin` | +| [List API Connectors](/docs/api/rest/reference/crc/ma/#operation/GetListAPIConnectors) | `crc:read:list_apiconnectors:master` | +| [Delete an API Connector](/docs/api/rest/reference/crc/methods/#operation/DeleteAPIConnector) | `crc:delete:apiconnector:admin` | +| [Delete an API Connector](/docs/api/rest/reference/crc/ma/#operation/DeleteAPIConnector) | `crc:delete:apiconnector:master` | +| [Create an API Connector](/docs/api/rest/reference/crc/methods/#operation/CreateAPIConnector) | `crc:write:apiconnector:admin` | +| [Create an API Connector](/docs/api/rest/reference/crc/ma/#operation/CreateAPIConnector) | `crc:write:apiconnector:master` | +| [Update an API Connector's private key](/docs/api/rest/reference/crc/methods/#operation/UpdateAPIConnectorPrivateKey) | `crc:update:apiconnector_private_key:admin` | +| [Update an API Connector's private key](/docs/api/rest/reference/crc/ma/#operation/UpdateAPIConnectorPrivateKey) | `crc:update:apiconnector_private_key:master` | +| [Update an API Connector](/docs/api/rest/reference/crc/methods/#operation/UpdateAPIConnector) | `crc:update:apiconnector:admin` | +| [Update an API Connector](/docs/api/rest/reference/crc/ma/#operation/UpdateAPIConnector) | `crc:update:apiconnector:master` | + +### Cisco/Polycom Rooms + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a Managed Room](/docs/api/rest/reference/crc/methods/#operation/GetaManagedRoom) | `crc:read:room:admin` | +| [Get a Managed Room](/docs/api/rest/reference/crc/ma/#operation/GetaManagedRoom) | `crc:read:room:master` | +| [Delete a managed room](/docs/api/rest/reference/crc/methods/#operation/Deleteamanagedroom) | `crc:delete:room:admin` | +| [Delete a managed room](/docs/api/rest/reference/crc/ma/#operation/Deleteamanagedroom) | `crc:delete:room:master` | +| [List Managed Rooms](/docs/api/rest/reference/crc/methods/#operation/ListManagedRooms) | `crc:read:list_rooms:admin` | +| [List Managed Rooms](/docs/api/rest/reference/crc/ma/#operation/ListManagedRooms) | `crc:read:list_rooms:master` | +| [Update a Managed Room](/docs/api/rest/reference/crc/methods/#operation/UpdateaManagedRoom) | `crc:update:room:admin` | +| [Update a Managed Room](/docs/api/rest/reference/crc/ma/#operation/UpdateaManagedRoom) | `crc:update:room:master` | +| [Create a Managed Room](/docs/api/rest/reference/crc/methods/#operation/CreateaManagedRoom) | `crc:write:room:admin` | +| [Create a Managed Room](/docs/api/rest/reference/crc/ma/#operation/CreateaManagedRoom) | `crc:write:room:master` | + +### Participant + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get participant identifier code](/docs/api/rest/reference/crc/methods/#operation/get_participant_identifier_code) | `crc:read:participant_identifier_code:admin`, `crc:read:participant_identifier_code:master` | + +### Room Template + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a room template](/docs/api/rest/reference/crc/methods/#operation/Deletearoomtemplate) | `crc:delete:rooms_template:admin` | +| [Delete a room template](/docs/api/rest/reference/crc/ma/#operation/Deletearoomtemplate) | `crc:delete:rooms_template:master` | +| [List Room Templates](/docs/api/rest/reference/crc/methods/#operation/ListRoomTemplates) | `crc:read:list_rooms_templates:admin` | +| [List Room Templates](/docs/api/rest/reference/crc/ma/#operation/ListRoomTemplates) | `crc:read:list_rooms_templates:master` | +| [Get a Room Template](/docs/api/rest/reference/crc/methods/#operation/GetaRoomTemplate) | `crc:read:rooms_template:admin` | +| [Get a Room Template](/docs/api/rest/reference/crc/ma/#operation/GetaRoomTemplate) | `crc:read:rooms_template:master` | +| [Create a Room Template](/docs/api/rest/reference/crc/methods/#operation/CreateaRoomTemplate) | `crc:write:rooms_template:admin` | +| [Create a Room Template](/docs/api/rest/reference/crc/ma/#operation/CreateaRoomTemplate) | `crc:write:rooms_template:master` | +| [Update a Room Template](/docs/api/rest/reference/crc/methods/#operation/UpdateaRoomTemplate) | `crc:update:rooms_template:admin` | +| [Update a Room Template](/docs/api/rest/reference/crc/ma/#operation/UpdateaRoomTemplate) | `crc:update:rooms_template:master` | + +## Contact Center + + +### Address Books + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Createacustomfield) | `contact_center:write:address_book_custom_field:admin` | +| [Get an address book](/docs/api/rest/reference/contact-center/methods/#operation/getAddressBook) | `contact_center:read:address_book:admin` | +| [Create an address book](/docs/api/rest/reference/contact-center/methods/#operation/createAddressBook) | `contact_center:write:address_book:admin` | +| [Get an address book's custom field](/docs/api/rest/reference/contact-center/methods/#operation/Getaaddressbookcustomfield) | `contact_center:read:address_book_custom_field:admin` | +| [Update an address book](/docs/api/rest/reference/contact-center/methods/#operation/updateAddressBook) | `contact_center:update:address_book:admin` | +| [Get an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/getUnit) | `contact_center:read:address_book_unit:admin` | +| [Delete an address book](/docs/api/rest/reference/contact-center/methods/#operation/deleteAddressBook) | `contact_center:delete:address_book:admin` | +| [Delete an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/deleteUnit) | `contact_center:delete:address_book_unit:admin` | +| [Delete an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Deleteancustomfield) | `contact_center:delete:address_book_custom_field:admin` | +| [List address book units](/docs/api/rest/reference/contact-center/methods/#operation/listUnits) | `contact_center:read:list_address_book_units:admin` | +| [Update an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/updateContact) | `contact_center:update:address_book_contact:admin` | +| [List an address book's custom fields](/docs/api/rest/reference/contact-center/methods/#operation/Listaddressbookcustomfields) | `contact_center:read:address_book_custom_field:admin` | +| [Create an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/createContact) | `contact_center:write:address_book_contact:admin` | +| [Update an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/updateUnit) | `contact_center:update:address_book_unit:admin` | +| [Create an address book unit](/docs/api/rest/reference/contact-center/methods/#operation/createUnit) | `contact_center:write:address_book_unit:admin` | +| [List address book contacts](/docs/api/rest/reference/contact-center/methods/#operation/listContacts) | `contact_center:read:list_address_book_contacts:admin` | +| [Get an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/getContact) | `contact_center:read:address_book_contact:admin` | +| [List address books](/docs/api/rest/reference/contact-center/methods/#operation/listAddressBooks) | `contact_center:read:list_address_books:admin` | +| [Update an address book custom field](/docs/api/rest/reference/contact-center/methods/#operation/Updateacustomfield) | `contact_center:update:address_book_custom_field:admin` | +| [Delete an address book contact](/docs/api/rest/reference/contact-center/methods/#operation/contactDelete) | `contact_center:delete:address_book_contact:admin` | +| [List a contact's custom fields](/docs/api/rest/reference/contact-center/methods/#operation/ListContactCustomFields) | `contact_center:read:address_book_custom_field:admin` | + +### Agent Statuses + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a system status](/docs/api/rest/reference/contact-center/methods/#operation/getAStatus) | `contact_center:read:system_status:admin` | +| [Create a system status](/docs/api/rest/reference/contact-center/methods/#operation/createSystemStatus) | `contact_center:write:system_status:admin` | +| [Update a system status](/docs/api/rest/reference/contact-center/methods/#operation/updateSystemStatus) | `contact_center:update:system_status:admin` | +| [Delete a system status](/docs/api/rest/reference/contact-center/methods/#operation/deleteSystemStatus) | `contact_center:delete:system_status:admin` | +| [List system statuses](/docs/api/rest/reference/contact-center/methods/#operation/listSystemStatus) | `contact_center:read:list_system_statues:admin` | + +### Asset Library + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List assets](/docs/api/rest/reference/contact-center/methods/#operation/listAssets) | `contact_center:read:asset_library:admin` | +| [Update an asset category](/docs/api/rest/reference/contact-center/methods/#operation/updateAnAssetCategory) | `contact_center:write:asset_library:admin` | +| [Create an asset](/docs/api/rest/reference/contact-center/methods/#operation/createAnAsset) | `contact_center:write:asset_library:admin` | +| [List asset categories](/docs/api/rest/reference/contact-center/methods/#operation/listAssetCategories) | `contact_center:read:asset_library:admin` | +| [Update an asset](/docs/api/rest/reference/contact-center/methods/#operation/updateAnAsset) | `contact_center:write:asset_library:admin` | +| [Delete an asset category](/docs/api/rest/reference/contact-center/methods/#operation/deleteAnAssetCategory) | `contact_center:delete:asset_library:admin` | +| [Delete an asset](/docs/api/rest/reference/contact-center/methods/#operation/deleteAnAsset) | `contact_center:delete:asset_library:admin` | +| [Get an asset](/docs/api/rest/reference/contact-center/methods/#operation/getAnAsset) | `contact_center:read:asset_library:admin` | +| [Create an asset category](/docs/api/rest/reference/contact-center/methods/#operation/createAnAssetCategory) | `contact_center:write:asset_library:admin` | +| [Duplicate an asset](/docs/api/rest/reference/contact-center/methods/#operation/duplicateAnAsset) | `contact_center:write:asset_library:admin` | +| [Delete asset items](/docs/api/rest/reference/contact-center/methods/#operation/Deleteassetitems) | `contact_center:write:asset_library:admin` | +| [Get an asset category](/docs/api/rest/reference/contact-center/methods/#operation/getAnAssetCategory) | `contact_center:read:asset_library:admin` | + +### Call Control + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Command control of a user](/docs/api/rest/reference/contact-center/methods/#operation/userControl) | `contact_center:write:user_control:admin` | +| [List user's devices](/docs/api/rest/reference/contact-center/methods/#operation/Listuserdevices) | `contact_center:read:user_device:admin` | +| [Control an engagement's recording](/docs/api/rest/reference/contact-center/methods/#operation/engagementRecordingControl) | `contact_center:update:engagement_recording_control:admin` | + +### Campaigns + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/createOutboundCampaign) | `contact_center:write:outbound_campaign:admin` | +| [List campaign contact list contacts](/docs/api/rest/reference/contact-center/methods/#operation/listCampaignContactListContacts) | `contact_center:read:outbound_campaign_contacts:admin` | +| [Get a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/getCampaignContactList) | `contact_center:read:outbound_campaign_contactlist:admin` | +| [Delete an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/deleteOutboundCampaign) | `contact_center:delete:outbound_campaign:admin` | +| [Create a campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/createCampaignContactListContact) | `contact_center:write:outbound_campaign_contacts:admin` | +| [Update an outbound campaign status](/docs/api/rest/reference/contact-center/methods/#operation/Updateanoutboundcampaignstatus) | `contact_center:update:outbound_campaign:admin` | +| [Update a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/updateCampaignContactList) | `contact_center:update:outbound_campaign_contactlist:admin` | +| [Get an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/getOutboundCampaign) | `contact_center:read:outbound_campaign:admin` | +| [Get a campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/getCampaignContactListContact) | `contact_center:read:outbound_campaign_contacts:admin` | +| [Create a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/createCampaignContactList) | `contact_center:write:outbound_campaign_contactlist:admin` | +| [List campaign contact lists](/docs/api/rest/reference/contact-center/methods/#operation/listCampaignContactLists) | `contact_center:read:outbound_campaign_contactlist:admin` | +| [List outbound campaigns](/docs/api/rest/reference/contact-center/methods/#operation/listOutboundCampaigns) | `contact_center:read:outbound_campaign:admin` | +| [Update contact on a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/updateCampaignContactListContact) | `contact_center:update:outbound_campaign_contacts:admin` | +| [Remove a campaign contact list](/docs/api/rest/reference/contact-center/methods/#operation/deleteCampaignContactList) | `contact_center:delete:outbound_campaign_contactlist:admin` | +| [Update an outbound campaign](/docs/api/rest/reference/contact-center/methods/#operation/updateOutboundCampaign) | `contact_center:update:outbound_campaign:admin` | +| [Remove campaign contact list's contact](/docs/api/rest/reference/contact-center/methods/#operation/deleteCampaigncontactListContact) | `contact_center:delete:outbound_campaign_contacts:admin` | + +### Dispositions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/createSet) | `contact_center:write:disposition_set:admin` | +| [List disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/listSets) | `contact_center:read:list_disposition_sets:admin` | +| [Get a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/getSet) | `contact_center:read:disposition_set:admin` | +| [Create a disposition](/docs/api/rest/reference/contact-center/methods/#operation/createDisposition) | `contact_center:write:disposition:admin` | +| [Delete a disposition](/docs/api/rest/reference/contact-center/methods/#operation/deleteDisposition) | `contact_center:delete:disposition:admin` | +| [Update a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/updateSet) | `contact_center:update:disposition_set:admin` | +| [List dispositions](/docs/api/rest/reference/contact-center/methods/#operation/listDispositions) | `contact_center:read:list_dispositions:admin` | +| [Delete a disposition set](/docs/api/rest/reference/contact-center/methods/#operation/deleteSet) | `contact_center:delete:disposition_set:admin` | +| [Update a disposition](/docs/api/rest/reference/contact-center/methods/#operation/updateDisposition) | `contact_center:update:disposition:admin` | +| [Get a disposition](/docs/api/rest/reference/contact-center/methods/#operation/getDisposition) | `contact_center:read:disposition:admin` | + +### Engagements + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Poll an engagement recording's status](/docs/api/rest/reference/contact-center/methods/#operation/EngagementRecordingStatus) | `contact_center:read:engagement_recording_status:admin` | +| [Update an engagement](/docs/api/rest/reference/contact-center/methods/#operation/updateEngagement) | `contact_center:update:engagement:admin` | +| [Get an engagement's events](/docs/api/rest/reference/contact-center/methods/#operation/getEngagementEvents) | `contact_center:read:engagement:admin` | +| [Start an engagement](/docs/api/rest/reference/contact-center/methods/#operation/Startworkitemengagement) | `contact_center:write:engagement:admin` | +| [List engagements](/docs/api/rest/reference/contact-center/methods/#operation/listEngagements) | `contact_center:read:list_engagements:admin` | +| [Get an engagement's survey](/docs/api/rest/reference/contact-center/methods/#operation/getEngagementSurvey) | `contact_center:read:engagement:admin` | +| [Get an engagement](/docs/api/rest/reference/contact-center/methods/#operation/getEngagement) | `contact_center:read:engagement:admin` | +| [Get an engagement's attachments](/docs/api/rest/reference/contact-center/methods/#operation/ListAttachments) | `contact_center:read:attachment:admin` | + +### Flows + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Import a flow](/docs/api/rest/reference/contact-center/methods/#operation/ImportFlow) | `contact_center:write:flow:admin` | +| [Remove flow entry points](/docs/api/rest/reference/contact-center/methods/#operation/RemoveFlowEntryPoints) | `contact_center:delete:flow:admin` | +| [List flows](/docs/api/rest/reference/contact-center/methods/#operation/listFlows) | `contact_center:read:list_flows:admin` | +| [List flow's entry points](/docs/api/rest/reference/contact-center/methods/#operation/ListFlowEntryPoints) | `contact_center:read:flow:admin` | +| [List entry points](/docs/api/rest/reference/contact-center/methods/#operation/ListentryPoints) | `contact_center:read:flow:admin` | +| [Add flow entry points](/docs/api/rest/reference/contact-center/methods/#operation/AddFlowEntryPoints) | `contact_center:write:flow:admin` | +| [Edit a flow](/docs/api/rest/reference/contact-center/methods/#operation/EditFlow) | `contact_center:update:flow:admin` | +| [Get a flow](/docs/api/rest/reference/contact-center/methods/#operation/getAFlow) | `contact_center:read:flow:admin` | +| [Delete a flow](/docs/api/rest/reference/contact-center/methods/#operation/DeleteFlow) | `contact_center:delete:flow:admin` | +| [Publish a flow](/docs/api/rest/reference/contact-center/methods/#operation/PublishFlow) | `contact_center:update:flow:admin` | +| [Export a flow](/docs/api/rest/reference/contact-center/methods/#operation/ExportFlow) | `contact_center:read:flow:admin` | + +### Inboxes + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create an inbox](/docs/api/rest/reference/contact-center/methods/#operation/inboxCreate) | `contact_center:write:inbox:admin` | +| [List an account's inbox messages](/docs/api/rest/reference/contact-center/methods/#operation/listInboxesMessages) | `contact_center:read:inbox_messages:admin` | +| [Unassign inbox access users](/docs/api/rest/reference/contact-center/methods/#operation/unassignInboxUsers) | `contact_center:delete:inbox_user:admin` | +| [Update an inbox](/docs/api/rest/reference/contact-center/methods/#operation/inboxUpdate) | `contact_center:update:inbox:admin` | +| [Delete inboxes](/docs/api/rest/reference/contact-center/methods/#operation/inboxesDelete) | `contact_center:delete:inbox:admin` | +| [Remove inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/unassignInboxQueues) | `contact_center:delete:inbox_queue:admin` | +| [Delete an inbox's messages](/docs/api/rest/reference/contact-center/methods/#operation/inboxMessagesDelete) | `contact_center:delete:inbox_messages:admin` | +| [Get inbox email notification list](/docs/api/rest/reference/contact-center/methods/#operation/Getinboxemailnotificationlist) | `contact_center:read:inbox:admin` | +| [Get an inbox's users](/docs/api/rest/reference/contact-center/methods/#operation/listInboxUsers) | `contact_center:read:inbox_user:admin` | +| [Delete inbox messages](/docs/api/rest/reference/contact-center/methods/#operation/inboxesMessagesDelete) | `contact_center:delete:inbox_messages:admin` | +| [Assign inbox access users](/docs/api/rest/reference/contact-center/methods/#operation/assignInboxUsers) | `contact_center:write:inbox_user:admin` | +| [List an inbox's messages](/docs/api/rest/reference/contact-center/methods/#operation/listInboxMessages) | `contact_center:read:inbox_messages:admin` | +| [Get inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/listInboxQueues) | `contact_center:read:list_inbox_queues:admin` | +| [Update an inbox email notification](/docs/api/rest/reference/contact-center/methods/#operation/Updateaninboxemailnotification) | `contact_center:update:inbox:admin` | +| [Assign inbox access queues](/docs/api/rest/reference/contact-center/methods/#operation/assignInboxQueues) | `contact_center:write:inbox_queue:admin` | +| [List inboxes](/docs/api/rest/reference/contact-center/methods/#operation/listInbox) | `contact_center:read:list_inboxes:admin` | +| [Delete an inbox message](/docs/api/rest/reference/contact-center/methods/#operation/inboxMessageDelete) | `contact_center:delete:inbox_message:admin` | +| [Get an inbox](/docs/api/rest/reference/contact-center/methods/#operation/getInbox) | `contact_center:read:inbox:admin` | + +### Logs + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List message history](/docs/api/rest/reference/contact-center/methods/#operation/getMessageHistory) | `contact_center:read:messaging:admin` | +| [List work item message history](/docs/api/rest/reference/contact-center/methods/#operation/getWorkItemMessageHistory) | `contact_center:read:engagement_log:admin` | +| [List voice call logs](/docs/api/rest/reference/contact-center/methods/#operation/listVoiceCall) | `contact_center:read:voice_call_log:admin` | +| [List voice call logs](/docs/api/rest/reference/contact-center/ma/#operation/listVoiceCall) | `contact_center:read:voice_call_log:master` | +| [List email message history](/docs/api/rest/reference/contact-center/methods/#operation/getEmailMessageHistory) | `contact_center:read:engagement_log:admin` | +| [List SMS logs](/docs/api/rest/reference/contact-center/methods/#operation/listSMS) | `contact_center:read:sms_log:admin` | +| [List SMS logs](/docs/api/rest/reference/contact-center/ma/#operation/listSMS) | `contact_center:read:sms_log:master` | + +### Messaging + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Send a message](/docs/api/rest/reference/contact-center/methods/#operation/SendaMessage) | `contact_center:write:messaging:admin` | + +### Notes + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List notes](/docs/api/rest/reference/contact-center/methods/#operation/notes) | `contact_center:read:list_notes:admin` | +| [Get a note](/docs/api/rest/reference/contact-center/methods/#operation/getNote) | `contact_center:read:note:admin` | +| [Update a note](/docs/api/rest/reference/contact-center/methods/#operation/noteUpdate) | `contact_center:update:note` | +| [List engagement notes](/docs/api/rest/reference/contact-center/methods/#operation/engagementNotes) | `contact_center:read:list_notes:admin` | + +### Operating Hours + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourCreate) | `contact_center:write:business_hour:admin` | +| [List the business hours' queues](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHourQueues) | `contact_center:read:business_hours_queue:admin` | +| [Update a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closureSetUpdate) | `contact_center:update:closure_hour:admin` | +| [Create a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closuresSetCreate) | `contact_center:write:closure_hour:admin` | +| [Update business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourUpdate) | `contact_center:update:business_hour:admin` | +| [List business hours](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHours) | `contact_center:read:list_business_hours:admin` | +| [List the closures' queues](/docs/api/rest/reference/contact-center/methods/#operation/listClosureSetQueues) | `contact_center:read:closure_hour_queue:admin` | +| [Get a closure set](/docs/api/rest/reference/contact-center/methods/#operation/getAClosureSet) | `contact_center:read:closure_hour:admin` | +| [List the business hours' flows](/docs/api/rest/reference/contact-center/methods/#operation/listBusinessHourFlows) | `contact_center:read:business_hours_flow:admin` | +| [Delete business hours](/docs/api/rest/reference/contact-center/methods/#operation/businessHourDelete) | `contact_center:delete:business_hour:admin` | +| [Get business hours](/docs/api/rest/reference/contact-center/methods/#operation/getABusinessHour) | `contact_center:read:business_hour:admin` | +| [Delete a closure set](/docs/api/rest/reference/contact-center/methods/#operation/closureSetDelete) | `contact_center:delete:closure_hour:admin` | +| [List closures](/docs/api/rest/reference/contact-center/methods/#operation/listClosures) | `contact_center:read:list_closure_hours:admin` | +| [List the closures' flows](/docs/api/rest/reference/contact-center/methods/#operation/listClosureSetFlows) | `contact_center:read:clousre_hour_flow:admin` | + +### Queues + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a queue's operating hours](/docs/api/rest/reference/contact-center/methods/#operation/QueueOperatingHoursUpdate) | `contact_center:patch:queue_operating_hours:admin` | +| [Update a queue agent](/docs/api/rest/reference/contact-center/methods/#operation/updateQueueAgent) | `contact_center:update:queue_agent:admin` | +| [Unassign a queue agent](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueAgent) | `contact_center:delete:queue_agent:admin` | +| [Assign queue agents](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueAgents) | `contact_center:write:queue_agent:admin` | +| [Delete a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueDelete) | `contact_center:delete:queue:admin` | +| [Assign queue dispositions](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueDispositions) | `contact_center:write:queue disposition:admin` | +| [Delete an attendee from a scheduled callback event](/docs/api/rest/reference/contact-center/methods/#operation/Deleteascheduledcallbackforanattendee) | `contact_center:delete:queue:admin` | +| [List queue dispositions](/docs/api/rest/reference/contact-center/methods/#operation/getQueueDispositions) | `contact_center:read:list_dispositions:admin` | +| [Batch delete queues](/docs/api/rest/reference/contact-center/methods/#operation/Batchdeletequeues) | `contact_center:delete:queue:admin` | +| [List queues](/docs/api/rest/reference/contact-center/methods/#operation/listQueues) | `contact_center:read:list_queues:admin` | +| [Update a queue's interrupt settings](/docs/api/rest/reference/contact-center/methods/#operation/updateQueueInterrupts) | `contact_center:update:queue:admin` | +| [Get a queue](/docs/api/rest/reference/contact-center/methods/#operation/getAQueue) | `contact_center:read:queue:admin` | +| [Unassign a queue disposition](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueDisposition) | `contact_center:delete:queue_disposition:admin` | +| [Assign queue teams](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueTeams) | `contact_center:write:queue_team:admin` | +| [Unassign a queue team](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueTeam) | `contact_center:delete:queue_team:admin` | +| [Create a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueCreate) | `contact_center:write:queue:admin` | +| [Delete a queue's interrupt menu configuration](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueInterruptMenu) | `contact_center:delete:queue:admin` | +| [Assign a queue menu based interrupt](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueMenuBasedInterrupt) | `contact_center:update:queue:admin` | +| [Unassign a queue supervisor](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueSupervisor) | `contact_center:delete:queue_supervisor:admin` | +| [Update a queue](/docs/api/rest/reference/contact-center/methods/#operation/queueUpdate) | `contact_center:update:queue:admin` | +| [Unassign a queue disposition set](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueDispositionSet) | `contact_center:delete:queue_disposition_set:admin` | +| [List queue templates](/docs/api/rest/reference/contact-center/methods/#operation/Listqueuetemplates) | `contact_center:read:queue:admin` | +| [Unassign multiple teams in a queue](/docs/api/rest/reference/contact-center/methods/#operation/batchDeleteQueueTeams) | `contact_center:delete:queue_team:admin` | +| [List queue disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/getQueueDispositionSets) | `contact_center:read:list_disposition_sets:admin` | +| [Batch create queues with a template](/docs/api/rest/reference/contact-center/methods/#operation/Batchcreatequeueswithatemplate) | `contact_center:write:queue:admin` | +| [Get a queue's operating hours](/docs/api/rest/reference/contact-center/methods/#operation/getAQueueOperatingHours) | `contact_center:read:queue_operating_hours:admin` | +| [List queue agents](/docs/api/rest/reference/contact-center/methods/#operation/getQueueAgents) | `contact_center:read:list_queue_agents:admin` | +| [Schedule a callback on a queue](/docs/api/rest/reference/contact-center/methods/#operation/Scheduleacallbackonaqueue) | `contact_center:write:queue:admin` | +| [Assign queue supervisors](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueSupervisors) | `contact_center:delete:queue_supervisor:admin` | +| [Assign queue disposition sets](/docs/api/rest/reference/contact-center/methods/#operation/assignQueueDispositionSets) | `contact_center:write:queue_disposition_set:admin` | +| [List queue supervisors](/docs/api/rest/reference/contact-center/methods/#operation/getQueueSupervisors) | `contact_center:delete:queue_supervisor:admin` | +| [List a queue's scheduled callbacks availability](/docs/api/rest/reference/contact-center/methods/#operation/Listqueuescheduledcallbacksavailability) | `contact_center:read:queue:admin` | + +### Recordings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete engagement recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteEngagementRecordings) | `contact_center:delete:recording:admin` | +| [List a user's recordings](/docs/api/rest/reference/contact-center/methods/#operation/listUserRecordings) | `contact_center:read:list_recordings:admin`, `contact_center:read:list_recordings` | +| [Delete a recording](/docs/api/rest/reference/contact-center/methods/#operation/deleteRecording) | `contact_center:delete:recording:admin` | +| [List recordings](/docs/api/rest/reference/contact-center/methods/#operation/listRecordings) | `contact_center:read:list_recordings:admin` | +| [List engagement recordings](/docs/api/rest/reference/contact-center/methods/#operation/listEngagementRecordings) | `contact_center:read:list_recordings:admin` | +| [List queue recordings](/docs/api/rest/reference/contact-center/methods/#operation/listQueueRecordings) | `contact_center:read:list_recordings:admin`, `contact_center:read:list_recordings` | +| [Delete queue recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteQueueRecordings) | `contact_center:delete:recording:admin` | +| [Delete a user's recordings](/docs/api/rest/reference/contact-center/methods/#operation/deleteUserRecordings) | `contact_center:delete:recording:admin` | + +### Regions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List regions](/docs/api/rest/reference/contact-center/methods/#operation/ListRegions) | `contact_center:read:list_regions:admin` | +| [Update a region](/docs/api/rest/reference/contact-center/methods/#operation/UpdateARegion) | `contact_center:udpate:region:admin` | +| [Delete a region](/docs/api/rest/reference/contact-center/methods/#operation/DeleteARegion) | `contact_center:delete:region:admin` | +| [Assign users to a region](/docs/api/rest/reference/contact-center/methods/#operation/AssignUsersToARegion) | `contact_center:write:region_user:admin` | +| [Create a region](/docs/api/rest/reference/contact-center/methods/#operation/CreateARegion) | `contact_center:write:region:admin` | +| [Get a region](/docs/api/rest/reference/contact-center/methods/#operation/GetARegion) | `contact_center:read:region:admin` | +| [List a region's users](/docs/api/rest/reference/contact-center/methods/#operation/ListRegion'sUsers) | `contact_center:read:list_region_users:admin` | + +### Reports V2(CX Analytics) + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List historical queue performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalqueueperformancedatasetdata) | `contact_center:read:dataset_queue_performance:admin` | +| [List operation logs](/docs/api/rest/reference/contact-center/methods/#operation/listOperationLogs) | `contact_center:read:operation_logs:admin` | +| [List historical flow performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalflowperformancedatasetdata) | `contact_center:read:dataset_flow_performance:admin` | +| [List historical disposition dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricaldispositiondatasetdata) | `contact_center:read:dataset_disposition:admin` | +| [List historical agent timecard dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalagenttimecarddatasetdata) | `contact_center:read:dataset_agent_timecard:admin` | +| [List historical engagement dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listengagementdatasetdata) | `contact_center:read:dataset_engagement:admin` | +| [List historical Zoom Phone to Contact Center call journey data](/docs/api/rest/reference/contact-center/methods/#operation/ListhistoricalZoomphonetozcccalljourneydata) | `contact_center:read:call_journey_log:admin` | +| [List historical outbound dialer performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricaloutbounddialerperformancedatasetdata) | `contact_center:read:dataset_outbound_dialer_performance:admin` | +| [List historical engagement log data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalengagementlogs) | `contact_center:read:engagement_log:admin` | +| [List historical agent performance dataset data](/docs/api/rest/reference/contact-center/methods/#operation/Listhistoricalagentperformancedatasetdata) | `contact_center:read:dataset_agent_performance:admin` | + +### Reports(Legacy Reports) + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List historical queue's agents reports](/docs/api/rest/reference/contact-center/methods/#operation/listQueueAgentMetric) | `contact_center:read:agent_report:admin` | +| [List historical detail reports](/docs/api/rest/reference/contact-center/methods/#operation/listHistoricalDetailMetric) | `contact_center:read:engagement_report:admin` | +| [List agent leg reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentLegMetric) | `contact_center:read:agent_report:admin` | +| [List agent's status history reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentStatusHistory) | `contact_center:read:agent_status_report:admin` | +| [List agent's time sheet reports](/docs/api/rest/reference/contact-center/methods/#operation/listAgentTimeSheet) | `contact_center:read:agent_report:admin` | +| [List historical agent reports by queue](/docs/api/rest/reference/contact-center/methods/#operation/listQueueAgentsMetrics) | `contact_center:read:agent_report:admin` | +| [List historical queue reports](/docs/api/rest/reference/contact-center/methods/#operation/listHistoricalQueueMetric) | `contact_center:read:queue_report:admin` | + +### Roles + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign a role](/docs/api/rest/reference/contact-center/methods/#operation/deleteRoleUser) | `contact_center:delete:role_user:admin` | +| [Create a role](/docs/api/rest/reference/contact-center/methods/#operation/createRole) | `contact_center:write:role:admin` | +| [Delete a role](/docs/api/rest/reference/contact-center/methods/#operation/deleteRole) | `contact_center:delete:role:admin` | +| [Get a role](/docs/api/rest/reference/contact-center/methods/#operation/getRole) | `contact_center:read:role:admin` | +| [Update a role](/docs/api/rest/reference/contact-center/methods/#operation/updateRole) | `contact_center:update:role:admin` | +| [Delete role privileges](/docs/api/rest/reference/contact-center/methods/#operation/Deleteroleprivileges) | `contact_center:delete:role:admin` | +| [List users of a role](/docs/api/rest/reference/contact-center/methods/#operation/getRoleUsers) | `contact_center:read:list_role_users:admin` | +| [List roles](/docs/api/rest/reference/contact-center/methods/#operation/listRoles) | `contact_center:read:list_roles:admin` | +| [Assign a role](/docs/api/rest/reference/contact-center/methods/#operation/assignRoleUsers) | `contact_center:write:role_user:admin` | +| [Duplicate a role](/docs/api/rest/reference/contact-center/methods/#operation/Duplicatearole) | `contact_center:write:role:admin` | + +### Routing Profiles + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Getaconsumerroutingprofile) | `contact_center:read:consumer_routing_profile:admin` | +| [Update a consumer routing profile's details](/docs/api/rest/reference/contact-center/methods/#operation/Updateaconsumerroutingprofile'sdetails) | `contact_center:update:consumer_routing_profile:admin` | +| [List consumer routing profiles](/docs/api/rest/reference/contact-center/methods/#operation/Listconsumerroutingprofiles) | `contact_center:read:consumer_routing_profile:admin` | +| [Create a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Createaconsumerroutingprofile) | `contact_center:write:consumer_routing_profile:admin` | +| [Get an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/getAgentRoutingProfile) | `contact_center:read:agent_routing_profile:admin` | +| [List agent routing profiles](/docs/api/rest/reference/contact-center/methods/#operation/Listagentroutingprofiles) | `contact_center:read:agent_routing_profile:admin` | +| [Update an agent routing profile's details](/docs/api/rest/reference/contact-center/methods/#operation/Updateanagentroutingprofile'sdetails) | `contact_center:update:agent_routing_profile:admin` | +| [Delete a consumer routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Deleteaconsumerroutingprofile) | `contact_center:delete:consumer_routing_profile:admin` | +| [Delete an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Deleteanagentroutingprofile) | `contact_center:delete:agent_routing_profile:admin` | +| [Create an agent routing profile](/docs/api/rest/reference/contact-center/methods/#operation/Createanagentroutingprofile) | `contact_center:write:agent_routing_profile:admin` | + +### SMS + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Send an SMS](/docs/api/rest/reference/contact-center/methods/#operation/contactCenterSMS) | `contact_center:write:sms:admin` | + +### Skills + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a skill category](/docs/api/rest/reference/contact-center/methods/#operation/getSkillCategory) | `contact_center:read:skill_category:admin` | +| [Create a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillCreate) | `contact_center:write:skill:admin` | +| [Delete a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillDelete) | `contact_center:delete:skill:admin` | +| [Update a skill](/docs/api/rest/reference/contact-center/methods/#operation/skillNameUpdate) | `contact_center:update:skill:admin` | +| [List users of a skill](/docs/api/rest/reference/contact-center/methods/#operation/listSkillUsers) | `contact_center:read:list_skill_users:admin` | +| [List skills](/docs/api/rest/reference/contact-center/methods/#operation/listSkills) | `contact_center:read:list_skills:admin` | +| [Create a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryCreate) | `contact_center:write:skill_category:admin` | +| [Get a skill](/docs/api/rest/reference/contact-center/methods/#operation/getSkill) | `contact_center:read:skill:admin` | +| [Delete a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryDelete) | `contact_center:delete:skill_category:admin` | +| [Update a skill category](/docs/api/rest/reference/contact-center/methods/#operation/SkillCategoryUpdate) | `contact_center:update:skill_category:admin` | +| [List skill categories](/docs/api/rest/reference/contact-center/methods/#operation/listSkillCategory) | `contact_center:read:list_skill_categories:admin` | + +### Teams + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/unassignTeamSupervisors) | `contact_center:delete:team:admin` | +| [Get a team](/docs/api/rest/reference/contact-center/methods/#operation/getTeamDetail) | `contact_center:read:team:admin` | +| [List teams](/docs/api/rest/reference/contact-center/methods/#operation/listTeams) | `contact_center:read:team:admin` | +| [List team's parent teams](/docs/api/rest/reference/contact-center/methods/#operation/getTeamParentTeams) | `contact_center:read:team:admin` | +| [List team agents](/docs/api/rest/reference/contact-center/methods/#operation/listTeamAgents) | `contact_center:read:team:admin` | +| [Create a team](/docs/api/rest/reference/contact-center/methods/#operation/CreateTeam) | `contact_center:write:team:admin` | +| [Move a team](/docs/api/rest/reference/contact-center/methods/#operation/moveTeam) | `contact_center:update:team:admin` | +| [Delete a team](/docs/api/rest/reference/contact-center/methods/#operation/deleteTeam) | `contact_center:delete:team:admin` | +| [List a team's child teams](/docs/api/rest/reference/contact-center/methods/#operation/getTeamChildTeams) | `contact_center:read:team:admin` | +| [Assign team agents](/docs/api/rest/reference/contact-center/methods/#operation/assignTeamAgents) | `contact_center:write:team:admin` | +| [Update a team](/docs/api/rest/reference/contact-center/methods/#operation/Updateateam) | `contact_center:update:team:admin` | +| [Assign team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/assignTeamSupervisors) | `contact_center:write:team:admin` | +| [List team supervisors](/docs/api/rest/reference/contact-center/methods/#operation/listTeamSupervisors) | `contact_center:read:team:admin` | +| [Unassign team agents](/docs/api/rest/reference/contact-center/methods/#operation/unassignTeamAgents) | `contact_center:delete:team:admin` | + +### Users + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Batch update user status](/docs/api/rest/reference/contact-center/methods/#operation/Batchupdateuserstatus) | `contact_center:write:batch_users:admin` | +| [Create a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/createUser) | `contact_center:write:user:admin` | +| [Update a user's status](/docs/api/rest/reference/contact-center/methods/#operation/Updateauser'sstatus) | `contact_center:update:user:admin` | +| [Batch create user profiles](/docs/api/rest/reference/contact-center/methods/#operation/BatchCreateUsers) | `contact_center:write:batch_users:admin` | +| [Batch delete user profiles](/docs/api/rest/reference/contact-center/methods/#operation/batchDeleteUsers) | `contact_center:delete:batch_users:admin` | +| [Get a user template](/docs/api/rest/reference/contact-center/methods/#operation/Getanusertemplate) | `contact_center:read:user_templates:admin` | +| [List user templates](/docs/api/rest/reference/contact-center/methods/#operation/ListUserTemplates) | `contact_center:read:user_templates:admin` | +| [Delete a user template](/docs/api/rest/reference/contact-center/methods/#operation/deleteAUserTemplate) | `contact_center:delete:user_templates:admin` | +| [Unassign user's skill](/docs/api/rest/reference/contact-center/methods/#operation/deleteASkill) | `contact_center:delete:user_skill:admin` | +| [List user's queues](/docs/api/rest/reference/contact-center/methods/#operation/listUserQueues) | `contact_center:read:list_user_queues:admin` | +| [Assign user's skills](/docs/api/rest/reference/contact-center/methods/#operation/assignSkills) | `contact_center:write:user_skill:admin` | +| [Update a user template](/docs/api/rest/reference/contact-center/methods/#operation/updateAUserTemplate) | `contact_center:update:user_templates:admin` | +| [List user's skills](/docs/api/rest/reference/contact-center/methods/#operation/ListAUserSkills) | `contact_center:read:list_user_skills:admin` | +| [Create a user template](/docs/api/rest/reference/contact-center/methods/#operation/createAUserTemplate) | `contact_center:write:user_templates:admin` | +| [Update a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userUpdate) | `contact_center:update:user:admin` | +| [Delete a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userDelete) | `contact_center:delete:user:admin` | +| [Get a user's profile](/docs/api/rest/reference/contact-center/methods/#operation/userGet) | `contact_center:read:user:admin` | +| [Batch update user profiles](/docs/api/rest/reference/contact-center/methods/#operation/BatchUpdateUsers) | `contact_center:update:batch_users:admin` | +| [List users' profiles](/docs/api/rest/reference/contact-center/methods/#operation/users) | `contact_center:read:list_users:admin` | + +### Variables + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableDelete) | `contact_center:delete:variable:admin` | +| [List variables](/docs/api/rest/reference/contact-center/methods/#operation/variables) | `contact_center:read:list_variables:admin` | +| [List variable groups](/docs/api/rest/reference/contact-center/methods/#operation/listVariableGroups) | `contact_center:read:list_variable_groups:admin` | +| [Get a variable log](/docs/api/rest/reference/contact-center/methods/#operation/getVariableLog) | `contact_center:read:variable_log:admin` | +| [Get a variable group](/docs/api/rest/reference/contact-center/methods/#operation/getAVariableGroup) | `contact_center:read:variable_group:admin` | +| [List variable logs](/docs/api/rest/reference/contact-center/methods/#operation/listVariableLogs) | `contact_center:read:list_variable_logs:admin` | +| [Create a variable](/docs/api/rest/reference/contact-center/methods/#operation/createVariable) | `contact_center:write:variable:admin` | +| [Delete a variable group](/docs/api/rest/reference/contact-center/methods/#operation/DeleteGroup) | `contact_center:delete:variable_group:admin` | +| [Delete a variable log](/docs/api/rest/reference/contact-center/methods/#operation/deleteVariableLog) | `contact_center:delete:variable_log:admin` | +| [Update a variable group](/docs/api/rest/reference/contact-center/methods/#operation/updateVariableGroup) | `contact_center:update:variable_group:admin` | +| [Get a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableGet) | `contact_center:read:variable:admin` | +| [Create a variable group](/docs/api/rest/reference/contact-center/methods/#operation/createVariableGroup) | `contact_center:write:variable_group:admin` | +| [Update a variable](/docs/api/rest/reference/contact-center/methods/#operation/variableUpdate) | `contact_center:update:variable:admin` | + +## Docs + + +### File Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a new file](/docs/api/rest/reference/Docs/methods/#operation/CreateDoc) | `docs:write:file`, `docs:write:file:admin` | +| [Modify metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/ModifyMetadata) | `docs:update:file`, `docs:update:file:admin` | +| [Get metadata of a file](/docs/api/rest/reference/Docs/methods/#operation/QueryFileMetadata) | `docs:read:file`, `docs:read:file:admin` | +| [List all children of a file](/docs/api/rest/reference/Docs/methods/#operation/ListAllChildren) | `docs:read:list_children`, `docs:read:list_children:admin` | +| [Delete a file](/docs/api/rest/reference/Docs/methods/#operation/DeleteFile) | `docs:delete:file`, `docs:delete:file:admin` | + +### File Uploads + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create file upload for docs import or attachments](/docs/api/rest/reference/Docs/methods/#operation/Uploadfilefordocsimportorattachments) | `docs:write:file_uploads`, `docs:write:file_uploads:admin` | + +### Import + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get file import status](/docs/api/rest/reference/Docs/methods/#operation/Getdocsfileimportstatus) | `docs:read:import`, `docs:read:import:admin` | +| [Create a new file by import](/docs/api/rest/reference/Docs/methods/#operation/Createanewfilebyimport) | `docs:write:import`, `docs:write:import:admin` | + +## Marketplace + + +### App + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List apps](/docs/api/rest/reference/marketplace/methods/#operation/ListApps) | `marketplace:read:list_apps:admin` | +| [Get an app's user requests](/docs/api/rest/reference/marketplace/methods/#operation/getAppUserRequests) | `marketplace:read:app_request:admin` | +| [Update app's request status](/docs/api/rest/reference/marketplace/methods/#operation/updateAppRequestStatus) | `marketplace:update:app_request:admin` | +| [Update app pre approval setting](/docs/api/rest/reference/marketplace/methods/#operation/Updateapppreapprovalsetting) | `marketplace:write:app_pre_approve:admin` | +| [Generate Zoom App Deeplink](/docs/api/rest/reference/marketplace/methods/#operation/GenerateZoomAppDeeplink) | `marketplace:write:app_deeplink:admin` | +| [Get user's custom field values](/docs/api/rest/reference/marketplace/methods/#operation/getCustomFieldValues) | `marketplace:read:custom_fields:admin`, `marketplace:read:custom_fields` | +| [Get API call logs](/docs/api/rest/reference/marketplace/methods/#operation/Getapicalllogs) | `marketplace:read:list_api_logs:admin` | +| [Send app notifications](/docs/api/rest/reference/marketplace/methods/#operation/Sendappnotifications) | `marketplace:write:notifications` | +| [Create apps](/docs/api/rest/reference/marketplace/methods/#operation/CreateApps) | `marketplace:write:app`, `marketplace:write:app:admin` | +| [Create apps](/docs/api/rest/reference/marketplace/ma/#operation/CreateApps) | `marketplace:write:app:master` | +| [Get app user entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getAppUserEntitlementRequests) | `marketplace:read:list_user_entitlements`, `marketplace:read:list_user_entitlements:admin` | +| [Get a user's entitlements](/docs/api/rest/reference/marketplace/methods/#operation/getUserEntitlementRequests) | `marketplace:read:list_user_entitlements`, `marketplace:read:list_user_entitlements:admin` | +| [Get information about an app](/docs/api/rest/reference/marketplace/methods/#operation/getAppInfo) | `marketplace:read:app`, `marketplace:read:app:admin` | +| [Add app allow requests for users](/docs/api/rest/reference/marketplace/methods/#operation/AddAppAllowRequestsForUsers) | `marketplace:write:app_request:admin` | +| [Rotate client secret](/docs/api/rest/reference/marketplace/methods/#operation/RotateClientSecret) | `marketplace:update:client_secret` | +| [Get a user's app requests](/docs/api/rest/reference/marketplace/methods/#operation/getUserAppRequests) | `marketplace:read:list_user_app_requests:admin`, `marketplace:read:list_user_app_requests` | +| [Enable or disable user app subscription](/docs/api/rest/reference/marketplace/methods/#operation/Enable/Disableuserappsubscription) | `marketplace:write:app:admin` | +| [Deletes an app](/docs/api/rest/reference/marketplace/methods/#operation/deleteApp) | `marketplace:write:app`, `marketplace:write:app:admin` | + +### Manifest + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Export an app manifest from an existing app](/docs/api/rest/reference/marketplace/methods/#operation/getAppManifest) | `marketplace:read:app`, `marketplace:read:app:admin` | +| [Update an app by manifest](/docs/api/rest/reference/marketplace/methods/#operation/updateAppByManifest) | `marketplace:write:app`, `marketplace:write:app:admin` | +| [Validate an app manifest](/docs/api/rest/reference/marketplace/methods/#operation/validatingManifest) | `marketplace:read:app`, `marketplace:read:app:admin` | + +## QSS + + +### Dashboards + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List meeting participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardMeetingParticipantsQOSSummary) | `dashboard:read:list_meeting_participants_qos:admin` | +| [List meeting participants QoS Summary](/docs/api/rest/reference/qss/ma/#operation/dashboardMeetingParticipantsQOSSummary) | `dashboard:read:list_meeting_participants_qos:master` | +| [List webinar participants QoS Summary](/docs/api/rest/reference/qss/methods/#operation/dashboardWebinarParticipantsQOSSummary) | `dashboard:read:list_webinar_participants_qos:admin` | +| [List webinar participants QoS Summary](/docs/api/rest/reference/qss/ma/#operation/dashboardWebinarParticipantsQOSSummary) | `dashboard:read:list_webinar_participants_qos:master` | + +## SCIM + + +### Group + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a group](/docs/api/rest/reference/scim-api/methods/#operation/groupScim2Create) | `scim2:admin` | +| [List groups](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2List) | `scim2:admin` | +| [Delete a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Delete) | `scim2:admin` | +| [Get a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Get) | `scim2:admin` | +| [Update a group](/docs/api/rest/reference/scim-api/methods/#operation/groupSCIM2Update) | `scim2:admin` | + +### User + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Deactivate a user](/docs/api/rest/reference/scim-api/methods/#operation/userADSCIM2Deactivate) | `scim2:admin` | +| [Delete a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Delete) | `scim2:admin` | +| [Update a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Update) | `scim2:admin` | +| [Get a user](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2Get) | `scim2:admin` | +| [Create a user](/docs/api/rest/reference/scim-api/methods/#operation/userScim2Create) | `scim2:admin` | +| [List users](/docs/api/rest/reference/scim-api/methods/#operation/userSCIM2List) | `scim2:admin` | + +## Tasks + + +### Assignee + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add assignees to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksAssignees) | `tasks:write:assignees`, `tasks:write:assignees:admin` | +| [Get assignees of a task](/docs/api/rest/reference/Tasks/methods/#operation/GetAssigneesOfATask) | `tasks:read:assignees`, `tasks:read:assignees:admin` | +| [Remove Assignee from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskAssignee) | `tasks:delete:assignees`, `tasks:delete:assignees:admin` | + +### Collaborator + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add collaborators to a task](/docs/api/rest/reference/Tasks/methods/#operation/addTasksCollaborators) | `tasks:write:collaborators`, `tasks:write:collaborators:admin` | +| [Remove collaborator from task](/docs/api/rest/reference/Tasks/methods/#operation/removeTaskCollaborator) | `tasks:delete:collaborator:admin`, `tasks:delete:collaborator` | +| [Get collaborators of a task](/docs/api/rest/reference/Tasks/methods/#operation/Getcollaboratorsofatask) | `tasks:read:list_collaborators`, `tasks:read:list_collaborators:admin` | + +### Comment + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a task's comment](/docs/api/rest/reference/Tasks/methods/#operation/DeleteTaskComment) | `tasks:delete:comment:admin`, `tasks:delete:comment` | +| [Get a task's comments](/docs/api/rest/reference/Tasks/methods/#operation/GetAV1TasksComment) | `tasks:read:comments`, `tasks:read:comments:admin` | +| [Add a comment to task](/docs/api/rest/reference/Tasks/methods/#operation/addComment) | `tasks:write:comment:admin`, `tasks:write:comment` | + +### Tasks + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update task fields](/docs/api/rest/reference/Tasks/methods/#operation/updateTask) | `tasks:update:task`, `tasks:update:task:admin` | +| [Delete a task](/docs/api/rest/reference/Tasks/methods/#operation/deleteTask) | `tasks:delete:task:admin`, `tasks:delete:task` | +| [Create a new task](/docs/api/rest/reference/Tasks/methods/#operation/createTask) | `tasks:write:task` | +| [Get task details](/docs/api/rest/reference/Tasks/methods/#operation/getTaskDetail) | `tasks:read:task`, `tasks:read:task:admin` | +| [List tasks](/docs/api/rest/reference/Tasks/methods/#operation/getMyTasks) | `tasks:read:list_tasks:admin`, `tasks:read:list_tasks` | + +## Team Chat + + +### Chat Channel Mention Group + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add channel members to a mention group](/docs/api/rest/reference/chat/methods/#operation/addAChannelMembersToMentionGroup) | `team_chat:update:mention_group`, `team_chat:update:mention_group:admin` | +| [List channel mention groups](/docs/api/rest/reference/chat/methods/#operation/getChannelMentionGroup) | `team_chat:read:mention_group`, `team_chat:read:mention_group:admin` | +| [Remove channel mention group members](/docs/api/rest/reference/chat/methods/#operation/removeChannelMentionGroupMembers) | `team_chat:update:mention_group`, `team_chat:update:mention_group:admin` | +| [Update a channel mention group information](/docs/api/rest/reference/chat/methods/#operation/updateChannelMentionGroup) | `team_chat:update:mention_group`, `team_chat:update:mention_group:admin` | +| [Create a channel mention group](/docs/api/rest/reference/chat/methods/#operation/createChannelMentionGroup) | `team_chat:write:mention_group:admin`, `team_chat:write:mention_group` | +| [Delete a channel mention group](/docs/api/rest/reference/chat/methods/#operation/deleteAChannelMentionGroup) | `team_chat:delete:mention_group`, `team_chat:delete:mention_group:admin` | +| [List the members of a mention group](/docs/api/rest/reference/chat/methods/#operation/listTheMembersOfMentionGroup) | `team_chat:read:mention_group`, `team_chat:read:mention_group:admin` | + +### Chat Channels + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List channel members](/docs/api/rest/reference/chat/methods/#operation/listUserLevelChannelMembers) | `team_chat:read:list_members`, `team_chat:read:list_members:admin` | +| [Create a channel](/docs/api/rest/reference/chat/methods/#operation/createChannel) | `team_chat:write:user_channel`, `team_chat:write:user_channel:admin` | +| [List channel members (Groups)](/docs/api/rest/reference/chat/methods/#operation/listChannelMembersGroups) | `team_chat:read:list_groups:admin` | +| [Update a channel](/docs/api/rest/reference/chat/methods/#operation/updateUserLevelChannel) | `team_chat:update:channel`, `team_chat:update:channel:admin` | +| [Remove a member (group)](/docs/api/rest/reference/chat/methods/#operation/removeAMemberGroup) | `team_chat:delete:group:admin` | +| [Remove a member](/docs/api/rest/reference/chat/methods/#operation/removeAUserLevelChannelMember) | `team_chat:delete:member`, `team_chat:delete:member:admin` | +| [List channel activity logs](/docs/api/rest/reference/chat/methods/#operation/listAllChannelActivityLogs) | `team_chat:read:list_channel_activity_logs:admin` | +| [Leave a channel](/docs/api/rest/reference/chat/methods/#operation/leaveChannel) | `team_chat:delete:member`, `team_chat:delete:member:admin` | +| [Perform operations on channels](/docs/api/rest/reference/chat/methods/#operation/PerformOperationsOnChannels) | `team_chat:update:archive_channels`, `team_chat:update:archive_channels:admin` | +| [List user's channels](/docs/api/rest/reference/chat/methods/#operation/getChannels) | `team_chat:read:list_user_channels`, `team_chat:read:list_user_channels:admin` | +| [Invite channel members (Groups)](/docs/api/rest/reference/chat/methods/#operation/inviteChannelMembersGroups) | `team_chat:write:groups:admin` | +| [Join a channel](/docs/api/rest/reference/chat/methods/#operation/joinChannel) | `team_chat:write:member` | +| [Invite channel members](/docs/api/rest/reference/chat/methods/#operation/InviteUserLevelChannelMembers) | `team_chat:write:members`, `team_chat:write:members:admin` | +| [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getUserLevelChannel) | `team_chat:read:channel`, `team_chat:read:channel:admin` | +| [Delete a channel](/docs/api/rest/reference/chat/methods/#operation/deleteUserLevelChannel) | `team_chat:delete:channel`, `team_chat:delete:channel:admin` | +| [Batch remove members from a channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveChannelMembers) | `team_chat:delete:batch_members`, `team_chat:delete:batch_members:admin` | + +### Chat Channels (Account-level) + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List channel administrators](/docs/api/rest/reference/chat/methods/#operation/listChannelAdministrators) | `team_chat:read:list_administrators:admin` | +| [Get a channel](/docs/api/rest/reference/chat/methods/#operation/getChannel) | `team_chat:read:user_channel`, `team_chat:read:user_channel:admin` | +| [Invite channel members](/docs/api/rest/reference/chat/methods/#operation/inviteChannelMembers) | `team_chat:write:members`, `team_chat:write:members:admin` | +| [Batch demote channel administrators](/docs/api/rest/reference/chat/methods/#operation/batchDemoteChannelAdministrators) | `team_chat:delete:batch_administrators:admin` | +| [Search user's or account's channels](/docs/api/rest/reference/chat/methods/#operation/searchChannels) | `team_chat:write:search_channels`, `team_chat:write:search_channels:admin` | +| [Promote channel members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteChannelMembersAsAdmin) | `team_chat:write:administrator:admin` | +| [Remove a member](/docs/api/rest/reference/chat/methods/#operation/removeAChannelMember) | `team_chat:delete:member:admin` | +| [Batch delete channels](/docs/api/rest/reference/chat/methods/#operation/batchDeleteChannelsAccountLevel) | `team_chat:delete:channels`, `team_chat:delete:channels:admin` | +| [List channel members](/docs/api/rest/reference/chat/methods/#operation/listChannelMembers) | `team_chat:read:list_members`, `team_chat:read:list_members:admin` | +| [Delete a channel](/docs/api/rest/reference/chat/methods/#operation/deleteChannel) | `team_chat:delete:user_channel`, `team_chat:delete:user_channel:admin` | +| [Update retention policy of a channel](/docs/api/rest/reference/chat/methods/#operation/updateChannelRetention) | `team_chat:update:retention:admin` | +| [Get retention policy of a channel](/docs/api/rest/reference/chat/methods/#operation/getChannelRetention) | `team_chat:read:retention:admin` | +| [List channel activity logs](/docs/api/rest/reference/chat/methods/#operation/listChannelActivityLogs) | `team_chat:read:list_channel_activity_logs:admin` | +| [List account's public channels](/docs/api/rest/reference/chat/methods/#operation/getAccountChannels) | `team_chat:read:list_channels:admin` | +| [Update a channel](/docs/api/rest/reference/chat/methods/#operation/updateChannel) | `team_chat:update:user_channel`, `team_chat:update:user_channel:admin` | +| [Batch remove members from a user's channel](/docs/api/rest/reference/chat/methods/#operation/batchRemoveUserChannelMembers) | `team_chat:delete:member:admin` | + +### Chat Emoji + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a custom emoji](/docs/api/rest/reference/chat/methods/#operation/addACustomEmoji) | `team_chat:write:custom_emoji`, `team_chat:write:custom_emoji:admin` | +| [Delete a custom emoji](/docs/api/rest/reference/chat/methods/#operation/DeleteCustomEmoji) | `team_chat:delete:custom_emoji`, `team_chat:delete:custom_emoji:admin` | +| [List custom emojis](/docs/api/rest/reference/chat/methods/#operation/listCustomEmojis) | `team_chat:read:list_custom_emojis`, `team_chat:read:list_custom_emojis:admin` | + +### Chat Files + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Send a chat file](/docs/api/rest/reference/chat/methods/#operation/sendChatFile) | `team_chat:write:message_files`, `team_chat:write:message_files:admin` | +| [Get file info](/docs/api/rest/reference/chat/methods/#operation/getFileInfo) | `team_chat:read:file`, `team_chat:read:file:admin` | +| [Upload a chat file](/docs/api/rest/reference/chat/methods/#operation/uploadAChatFile) | `team_chat:write:files`, `team_chat:write:files:admin` | +| [Delete a chat file](/docs/api/rest/reference/chat/methods/#operation/deleteChatFile) | `team_chat:delete:file`, `team_chat:delete:file:admin` | + +### Chat Messages + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a message](/docs/api/rest/reference/chat/methods/#operation/getChatMessage) | `team_chat:read:user_message`, `team_chat:read:user_message:admin` | +| [Delete a scheduled message](/docs/api/rest/reference/chat/methods/#operation/deleteScheduleMessage) | `team_chat:delete:scheduled_message` | +| [List pinned history messages of channel](/docs/api/rest/reference/chat/methods/#operation/listChannelPinnedMessages) | `team_chat:read:list_pinned_messages` | +| [React to a chat message](/docs/api/rest/reference/chat/methods/#operation/reactMessage) | `team_chat:update:message_emoji`, `team_chat:update:message_emoji:admin` | +| [Delete a message](/docs/api/rest/reference/chat/methods/#operation/deleteChatMessage) | `team_chat:delete:user_message`, `team_chat:delete:user_message:admin` | +| [Perform operations on the message of channel](/docs/api/rest/reference/chat/methods/#operation/PerformMessageOfChannel) | `team_chat:update:pin_message` | +| [Update a message](/docs/api/rest/reference/chat/methods/#operation/editMessage) | `team_chat:update:user_message`, `team_chat:update:user_message:admin` | +| [List user's chat messages](/docs/api/rest/reference/chat/methods/#operation/getChatMessages) | `team_chat:read:list_user_messages`, `team_chat:read:list_user_messages:admin` | +| [Get a forwarded message](/docs/api/rest/reference/chat/methods/#operation/getForwardedMessage) | `team_chat:read:user_message`, `team_chat:read:user_message:admin` | +| [Add or remove a bookmark](/docs/api/rest/reference/chat/methods/#operation/addOrRemoveABookmark) | `team_chat:update:bookmark` | +| [List scheduled messages](/docs/api/rest/reference/chat/methods/#operation/listScheduledMessages) | `team_chat:read:list_scheduled_messages` | +| [Send a chat message](/docs/api/rest/reference/chat/methods/#operation/sendaChatMessage) | `team_chat:write:user_message`, `team_chat:write:user_message:admin` | +| [List bookmarks](/docs/api/rest/reference/chat/methods/#operation/fetchBookmarks) | `team_chat:read:list_bookmarks` | +| [Mark message read or unread](/docs/api/rest/reference/chat/methods/#operation/markMessage) | `team_chat:update:message_status`, `team_chat:update:message_status:admin` | +| [Retrieve a thread](/docs/api/rest/reference/chat/methods/#operation/retrieveThread) | `team_chat:read:thread_message`, `team_chat:read:thread_message:admin` | + +### Chat Migration + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get migrated Zoom channel IDs](/docs/api/rest/reference/chat/methods/#operation/getMigrationChannelsMapping) | `team_chat:read:migrated_channels:admin` | +| [Migrate channel members](/docs/api/rest/reference/chat/methods/#operation/MigrateChannelMembers) | `team_chat:write:migrate_channel_members:admin` | +| [Get migrated Zoom user IDs](/docs/api/rest/reference/chat/methods/#operation/getMigrationUsersMapping) | `team_chat:read:migrated_users:admin` | +| [Migrate chat messages](/docs/api/rest/reference/chat/methods/#operation/MigrateChatMessages) | `team_chat:write:migrate_chat_messages:admin` | +| [Migrate a chat channel](/docs/api/rest/reference/chat/methods/#operation/MigrateAChatChannel) | `team_chat:write:migrate_a_chat_channel:admin` | +| [Migrate chat message reactions](/docs/api/rest/reference/chat/methods/#operation/MigrateChatMessageReactions) | `team_chat:write:migrate_chat_message_reactions:admin` | + +### Chat Reminder + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List reminders](/docs/api/rest/reference/chat/methods/#operation/listReminders) | `team_chat:read:list_reminders` | +| [Create a reminder message](/docs/api/rest/reference/chat/methods/#operation/createReminderForMessage) | `team_chat:write:reminder` | +| [Delete a reminder for a message](/docs/api/rest/reference/chat/methods/#operation/deleteReminderForMessage) | `team_chat:delete:reminder` | + +### Chat Sessions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Star or unstar a channel or contact user](/docs/api/rest/reference/chat/methods/#operation/starUnstarChannelContact) | `team_chat:update:chat_control`, `team_chat:update:chat_control:admin` | +| [List a user's chat sessions](/docs/api/rest/reference/chat/methods/#operation/getChatSessions) | `team_chat:read:list_user_sessions`, `team_chat:read:list_user_sessions:admin` | + +### Contacts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get user's contact details](/docs/api/rest/reference/chat/methods/#operation/getUserContact) | `team_chat:read:contact` | +| [Search company contacts](/docs/api/rest/reference/chat/methods/#operation/searchCompanyContacts) | `contact:read:list_contacts`, `contact:read:list_contacts:admin` | +| [List user's contacts](/docs/api/rest/reference/chat/methods/#operation/getUserContacts) | `team_chat:read:list_contacts` | + +### IM Groups + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupUpdate) | `contact_group:update:group:admin` | +| [Update an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupUpdate) | `contact_group:update:group:master` | +| [Add IM directory group members](/docs/api/rest/reference/chat/methods/#operation/imGroupMembersCreate) | `contact_group:write:member:admin` | +| [Add IM directory group members](/docs/api/rest/reference/chat/ma/#operation/imGroupMembersCreate) | `contact_group:write:member:master` | +| [List IM directory groups](/docs/api/rest/reference/chat/methods/#operation/imGroups) | `contact_group:read:list_groups:admin` | +| [List IM directory groups](/docs/api/rest/reference/chat/ma/#operation/imGroups) | `contact_group:read:list_groups:master` | +| [Delete an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupDelete) | `contact_group:delete:group:admin` | +| [Delete an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupDelete) | `contact_group:delete:group:master` | +| [Retrieve an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroup) | `contact_group:read:group:admin` | +| [Retrieve an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroup) | `contact_group:read:group:master` | +| [Delete IM directory group member](/docs/api/rest/reference/chat/methods/#operation/imGroupMembersDelete) | `contact_group:delete:member:admin` | +| [Delete IM directory group member](/docs/api/rest/reference/chat/ma/#operation/imGroupMembersDelete) | `contact_group:delete:member:master` | +| [Create an IM directory group](/docs/api/rest/reference/chat/methods/#operation/imGroupCreate) | `contact_group:write:group:admin` | +| [Create an IM directory group](/docs/api/rest/reference/chat/ma/#operation/imGroupCreate) | `contact_group:write:group:master` | +| [List IM directory group members](/docs/api/rest/reference/chat/methods/#operation/imGroupMembers) | `contact_group:read:list_members:admin` | +| [List IM directory group members](/docs/api/rest/reference/chat/ma/#operation/imGroupMembers) | `contact_group:read:list_members:master` | + +### Invitations + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Send new contact invitation](/docs/api/rest/reference/chat/methods/#operation/sendNewContactInvitation) | `team_chat:write:contact_information:admin`, `team_chat:write:contact_information` | + +### Legal Hold + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a legal hold matter](/docs/api/rest/reference/chat/methods/#operation/addLegalHoldMatter) | `team_chat:write:legal_hold_matter:admin` | +| [Download legal hold files for given matter](/docs/api/rest/reference/chat/methods/#operation/downloadLegalHoldFiles) | `team_chat:read:legal_hold_matter_file:admin` | +| [List legal hold files by given matter](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldFiles) | `team_chat:read:list_legal_hold_matter_files:admin` | +| [Update legal hold matter](/docs/api/rest/reference/chat/methods/#operation/updateLegalHoldMatter) | `team_chat:update:legal_hold_matter:admin` | +| [List legal hold matters](/docs/api/rest/reference/chat/methods/#operation/listLegalHoldMatters) | `team_chat:read:list_legal_hold_matters:admin` | +| [Delete legal hold matters](/docs/api/rest/reference/chat/methods/#operation/deleteLegalHoldMatters) | `team_chat:delete:legal_hold_matter:admin` | + +### Reports + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get chat sessions reports](/docs/api/rest/reference/chat/methods/#operation/reportChatSessions) | `report:read:list_chat_sessions:admin` | +| [Get chat message reports](/docs/api/rest/reference/chat/methods/#operation/reportChatMessages) | `report:read:chat_session:admin` | + +### Shared Spaces + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove members from a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpaceMembers) | `team_chat:delete:shared_space_members`, `team_chat:delete:shared_space_members:admin` | +| [List shared spaces](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaces) | `team_chat:read:list_shared_spaces`, `team_chat:read:list_shared_spaces:admin` | +| [List shared space members](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceMembers) | `team_chat:read:list_shared_space_members`, `team_chat:read:list_shared_space_members:admin` | +| [Delete a shared space](/docs/api/rest/reference/chat/methods/#operation/deleteSpace) | `team_chat:delete:shared_space`, `team_chat:delete:shared_space:admin` | +| [Get a shared space](/docs/api/rest/reference/chat/methods/#operation/getASharedSpace) | `team_chat:read:shared_space`, `team_chat:read:shared_space:admin` | +| [Update shared space settings](/docs/api/rest/reference/chat/methods/#operation/updateSharedSpaceSettings) | `team_chat:update:shared_space`, `team_chat:update:shared_space:admin` | +| [Promote shared space members to administrators](/docs/api/rest/reference/chat/methods/#operation/promoteSpaceMembers) | `team_chat:write:shared_space_administrators`, `team_chat:write:shared_space_administrators:admin` | +| [List shared space channels](/docs/api/rest/reference/chat/methods/#operation/listSharedSpaceChannels) | `team_chat:read:list_shared_space_channels`, `team_chat:read:list_shared_space_channels:admin` | +| [Add members to a shared space](/docs/api/rest/reference/chat/methods/#operation/addSpaceMembers) | `team_chat:write:shared_space_members`, `team_chat:write:shared_space_members:admin` | +| [Demote shared space administrators to members](/docs/api/rest/reference/chat/methods/#operation/demoteSpaceAdmins) | `team_chat:delete:shared_space_administrators`, `team_chat:delete:shared_space_administrators:admin` | +| [Create a shared space](/docs/api/rest/reference/chat/methods/#operation/createSpace) | `team_chat:write:shared_space`, `team_chat:write:shared_space:admin` | +| [Move shared space channels](/docs/api/rest/reference/chat/methods/#operation/updateSharedSpaceChannels) | `team_chat:update:shared_space_channels` | +| [Transfer shared space ownership](/docs/api/rest/reference/chat/methods/#operation/transferSpaceOwner) | `team_chat:update:shared_space_owner`, `team_chat:update:shared_space_owner:admin` | + +## Whiteboard + + +### Archiving + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List whiteboards sessions](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsarchivefiles) | `whiteboard:read:list_sessions`, `whiteboard:read:list_sessions:admin` | +| [Download Whiteboards activity file](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardsactivityfile) | `whiteboard:read:archived_file`, `whiteboard:read:archived_file:admin` | +| [List whiteboard sessions activities](/docs/api/rest/reference/Whiteboard/methods/#operation/Listwhiteboardsessionsarchivedfiles) | `whiteboard:read:session`, `whiteboard:read:session:admin` | + +### Collaborator + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Share a whiteboard to new users or team chat channels.](/docs/api/rest/reference/Whiteboard/methods/#operation/AddAWhiteboardCollaborator) | `whiteboard:write:collaborator:admin` | +| [Update whiteboard collaborators](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardCollaborator) | `whiteboard:update:collaborator:admin` | +| [Get collaborators of a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/GetAWhiteboardCollaborator) | `whiteboard:read:list_collaborators:admin` | +| [Remove the collaborator from a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/DeleteAWhiteboardCollaborator) | `whiteboard:delete:collaborator:admin` | + +### Document + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/GetAWhiteboard) | `whiteboard:read:whiteboard:admin`, `whiteboard:read:whiteboard` | +| [Update whiteboard basic information](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardMetadata) | `whiteboard:update:whiteboard:admin` | +| [Create a new whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/newWhiteboardCreate) | `whiteboard:write:whiteboard` | +| [Delete a whiteboard](/docs/api/rest/reference/Whiteboard/methods/#operation/DeleteAWhiteboard) | `whiteboard:delete:whiteboard:admin`, `whiteboard:delete:whiteboard` | +| [List all whiteboards](/docs/api/rest/reference/Whiteboard/methods/#operation/ListWhiteboards) | `whiteboard:read:list_whiteboards:admin`, `whiteboard:read:list_whiteboards` | + +### Export + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Download whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadwhiteboardexport) | `whiteboard:read:export`, `whiteboard:read:export:admin` | +| [Get whiteboard export generation status](/docs/api/rest/reference/Whiteboard/methods/#operation/Getwhiteboardexportdatagenerationstatus) | `whiteboard:read:export`, `whiteboard:read:export:admin` | +| [Create whiteboard export](/docs/api/rest/reference/Whiteboard/methods/#operation/Createwhiteboardsexport) | `whiteboard:write:export`, `whiteboard:write:export:admin` | + +### File + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Download Imported Whiteboard File](/docs/api/rest/reference/Whiteboard/methods/#operation/Downloadembeddedwhiteboardfile) | `whiteboard:read:file`, `whiteboard:read:file:admin` | +| [Upload file for whiteboard import](/docs/api/rest/reference/Whiteboard/methods/#operation/Uploadfileforwhiteboardimport) | `whiteboard:write:file`, `whiteboard:write:file:admin` | + +### Import + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get whiteboard import status](/docs/api/rest/reference/Whiteboard/methods/#operation/GetWhiteboardimportstatus) | `whiteboard:read:import:admin`, `whiteboard:read:import` | +| [Create a new whiteboard by import](/docs/api/rest/reference/Whiteboard/methods/#operation/CreateWhiteboardImport) | `whiteboard:write:import`, `whiteboard:write:import:admin` | + +### Project + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Share a project to new users](/docs/api/rest/reference/Whiteboard/methods/#operation/Shareaprojecttonewusers) | `whiteboard:write:project_collaborator`, `whiteboard:write:project_collaborator:admin` | +| [Move whiteboards to a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Movewhiteboardstoproject) | `whiteboard:write:project_whiteboard`, `whiteboard:write:project_whiteboard:admin` | +| [Create a new project](/docs/api/rest/reference/Whiteboard/methods/#operation/Createproject) | `whiteboard:write:project`, `whiteboard:write:project:admin` | +| [Remove whiteboards from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removewhiteboardsfromaproject) | `whiteboard:delete:project_whiteboard`, `whiteboard:delete:project_whiteboard:admin` | +| [Remove the collaborator from a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Removethecollaboratorfromaproject) | `whiteboard:delete:project_collaborator:admin`, `whiteboard:delete:project_collaborator` | +| [Delete a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Deleteproject) | `whiteboard:delete:project`, `whiteboard:delete:project:admin` | +| [Update project basic information](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateproject) | `whiteboard:update:project:admin`, `whiteboard:update:project` | +| [List all projects](/docs/api/rest/reference/Whiteboard/methods/#operation/Listallprojects) | `whiteboard:read:list_projects:admin`, `whiteboard:read:list_projects` | +| [Get collaborators of a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getcollaboratorsofaproject) | `whiteboard:read:project_collaborator`, `whiteboard:read:project_collaborator:admin` | +| [Update project collaborators](/docs/api/rest/reference/Whiteboard/methods/#operation/Updateprojectcollaborators) | `whiteboard:update:project_collaborator`, `whiteboard:update:project_collaborator:admin` | +| [Get a project](/docs/api/rest/reference/Whiteboard/methods/#operation/Getaproject) | `whiteboard:read:project`, `whiteboard:read:project:admin` | + +### Settings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update whiteboard share setting](/docs/api/rest/reference/Whiteboard/methods/#operation/UpdateAWhiteboardShareSetting) | `whiteboard:update:share_setting:admin`, `whiteboard:update:share_setting` | + +## Zoom Account + + +### Accounts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Upload virtual background files](/docs/api/rest/reference/account/methods/#operation/uploadVB) | `account:write:virtual_background_files:master`, `account:write:virtual_background_files:admin` | +| [Upload virtual background files](/docs/api/rest/reference/account/ma/#operation/uploadVB) | `account:write:virtual_background_files:master` | +| [Update an account's webinar registration settings](/docs/api/rest/reference/account/methods/#operation/accountSettingsRegistrationUpdate) | `account:update:registration_settings:master` | +| [Update an account's webinar registration settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsRegistrationUpdate) | `account:update:registration_settings:master` | +| [Get sub account details](/docs/api/rest/reference/account/ma/#operation/GetSubAccountDetails) | `account:read:sub_account:master`, `account:read:sub_account:admin` | +| [Delete virtual background files](/docs/api/rest/reference/account/methods/#operation/delVB) | `account:delete:virtual_background_files:master`, `account:delete:virtual_background_files:admin` | +| [Delete virtual background files](/docs/api/rest/reference/account/ma/#operation/delVB) | `account:delete:virtual_background_files:master` | +| [List sub accounts](/docs/api/rest/reference/account/ma/#operation/accounts) | `account:read:list_sub_accounts:master` | +| [Get account settings](/docs/api/rest/reference/account/methods/#operation/accountSettings) | `account:read:settings:admin`, `account:read:settings:master` | +| [Get account settings](/docs/api/rest/reference/account/ma/#operation/accountSettings) | `account:read:settings:master` | +| [Update account settings](/docs/api/rest/reference/account/methods/#operation/accountSettingsUpdate) | `account:update:settings:admin`, `account:update:settings:master` | +| [Update account settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsUpdate) | `account:update:settings:master` | +| [Get an account's webinar registration settings](/docs/api/rest/reference/account/methods/#operation/accountSettingsRegistration) | `account:read:registration_settings:master` | +| [Get an account's webinar registration settings](/docs/api/rest/reference/account/ma/#operation/accountSettingsRegistration) | `account:read:registration_settings:master` | +| [Create a sub account](/docs/api/rest/reference/account/ma/#operation/accountCreate) | `account:write:sub_account:master` | +| [Update the account owner](/docs/api/rest/reference/account/methods/#operation/UpdateTheAccountOwner) | `account:update:owner:master`, `account:update:owner:admin` | +| [Update the account owner](/docs/api/rest/reference/account/ma/#operation/UpdateTheAccountOwner) | `account:update:owner:master` | +| [Update options](/docs/api/rest/reference/account/ma/#operation/UpdateOptions) | `account:update:options:master` | +| [Get account's managed domains](/docs/api/rest/reference/account/methods/#operation/accountManagedDomain) | `account:read:managed_domains:master` | +| [Get account's managed domains](/docs/api/rest/reference/account/ma/#operation/accountManagedDomain) | `account:read:managed_domains:master` | +| [Get account's trusted domains](/docs/api/rest/reference/account/methods/#operation/accountTrustedDomain) | `account:read:trusted_domains:master` | +| [Get account's trusted domains](/docs/api/rest/reference/account/ma/#operation/accountTrustedDomain) | `account:read:trusted_domains:master` | +| [Disassociate a sub account](/docs/api/rest/reference/account/ma/#operation/DisassociateASubAccount) | `account:delete:sub_account:master` | +| [Update locked settings](/docs/api/rest/reference/account/methods/#operation/UpdateLockedSettings) | `account:update:lock_settings:master`, `account:update:lock_settings:admin` | +| [Update locked settings](/docs/api/rest/reference/account/ma/#operation/UpdateLockedSettings) | `account:update:lock_settings:master` | +| [Get locked settings](/docs/api/rest/reference/account/methods/#operation/getAccountLockSettings) | `account:read:lock_settings:master` | +| [Get locked settings](/docs/api/rest/reference/account/ma/#operation/getAccountLockSettings) | `account:read:lock_settings:master` | + +### Dashboards + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get top 25 issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoomIssue) | `dashboard:read:issues_zoomroom:admin` | +| [Get top 25 issues of Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRoomIssue) | `dashboard:read:issues_zoomroom:master` | +| [List client meeting satisfaction](/docs/api/rest/reference/account/methods/#operation/listMeetingSatisfaction) | `dashboard:read:meeting_survey:admin` | +| [List client meeting satisfaction](/docs/api/rest/reference/account/ma/#operation/listMeetingSatisfaction) | `dashboard:read:meeting_survey:master` | +| [Get webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantQOS) | `dashboard:read:webinar_participant_qos:admin` | +| [Get webinar participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantQOS) | `dashboard:read:webinar_participant_qos:master` | +| [Get issues of Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardIssueDetailZoomRoom) | `dashboard:read:issues_zoomroom:admin` | +| [Get issues of Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardIssueDetailZoomRoom) | `dashboard:read:issues_zoomroom:master` | +| [Get zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedbackDetail) | `dashboard:read:meeting_feedback:admin` | +| [Get zoom meetings client feedback](/docs/api/rest/reference/account/ma/#operation/dashboardClientFeedbackDetail) | `dashboard:read:meeting_feedback:master` | +| [Get post webinar feedback](/docs/api/rest/reference/account/methods/#operation/participantWebinarFeedback) | `dashboard:read:post_webinar_feedback:admin` | +| [Get post webinar feedback](/docs/api/rest/reference/account/ma/#operation/participantWebinarFeedback) | `dashboard:read:post_webinar_feedback:master` | +| [Get post meeting feedback](/docs/api/rest/reference/account/methods/#operation/participantFeedback) | `dashboard:read:post_meeting_feedback:admin` | +| [Get post meeting feedback](/docs/api/rest/reference/account/ma/#operation/participantFeedback) | `dashboard:read:post_meeting_feedback:master` | +| [List meetings](/docs/api/rest/reference/account/methods/#operation/dashboardMeetings) | `dashboard:read:list_meetings:admin` | +| [List meetings](/docs/api/rest/reference/account/ma/#operation/dashboardMeetings) | `dashboard:read:list_meetings:master` | +| [Get Zoom Rooms details](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRoom) | `dashboard:read:zoomroom:admin` | +| [Get Zoom Rooms details](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRoom) | `dashboard:read:zoomroom:master` | +| [Get webinar participants](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipants) | `dashboard:read:list_webinar_participants:admin` | +| [Get webinar participants](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipants) | `dashboard:read:list_webinar_participants:master` | +| [Get top 25 Zoom Rooms with issues](/docs/api/rest/reference/account/methods/#operation/dashboardIssueZoomRoom) | `dashboard:read:list_zoomrooms:admin` | +| [Get top 25 Zoom Rooms with issues](/docs/api/rest/reference/account/ma/#operation/dashboardIssueZoomRoom) | `dashboard:read:list_zoomrooms:master` | +| [List Zoom Rooms](/docs/api/rest/reference/account/methods/#operation/dashboardZoomRooms) | `dashboard:read:list_zoomrooms:admin` | +| [List Zoom Rooms](/docs/api/rest/reference/account/ma/#operation/dashboardZoomRooms) | `dashboard:read:list_zoomrooms:master` | +| [Get webinar details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarDetail) | `dashboard:read:webinar:admin` | +| [Get webinar details](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarDetail) | `dashboard:read:webinar:master` | +| [List webinars](/docs/api/rest/reference/account/methods/#operation/dashboardWebinars) | `dashboard:read:list_webinars:admin` | +| [List webinars](/docs/api/rest/reference/account/ma/#operation/dashboardWebinars) | `dashboard:read:list_webinars:master` | +| [List Zoom meetings client feedback](/docs/api/rest/reference/account/methods/#operation/dashboardClientFeedback) | `dashboard:read:list_meetings_feedback:admin` | +| [List Zoom meetings client feedback](/docs/api/rest/reference/account/ma/#operation/dashboardClientFeedback) | `dashboard:read:list_meetings_feedback:master` | +| [Get meeting quality scores](/docs/api/rest/reference/account/methods/#operation/dashboardQuality) | `dashboard:read:meeting_quality_score:admin` | +| [Get meeting quality scores](/docs/api/rest/reference/account/ma/#operation/dashboardQuality) | `dashboard:read:meeting_quality_score:master` | +| [List webinar participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantsQOS) | `dashboard:read:list_webinar_participants_qos:admin` | +| [List webinar participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantsQOS) | `dashboard:read:list_webinar_participants_qos:master` | +| [List meeting participants QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantsQOS) | `dashboard:read:list_meeting_participants_qos:admin` | +| [List meeting participants QoS](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantsQOS) | `dashboard:read:list_meeting_participants_qos:master` | +| [Get CRC port usage](/docs/api/rest/reference/account/methods/#operation/dashboardCRC) | `dashboard:read:crc_port_usage:admin` | +| [Get CRC port usage](/docs/api/rest/reference/account/ma/#operation/dashboardCRC) | `dashboard:read:crc_port_usage:master` | +| [List the client versions](/docs/api/rest/reference/account/methods/#operation/getClientVersions) | `dashboard:read:client_versions:admin` | +| [List the client versions](/docs/api/rest/reference/account/ma/#operation/getClientVersions) | `dashboard:read:client_versions:master` | +| [Get webinar sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardWebinarParticipantShare) | `dashboard:read:webinar_sharing:admin` | +| [Get webinar sharing/recording details](/docs/api/rest/reference/account/ma/#operation/dashboardWebinarParticipantShare) | `dashboard:read:webinar_sharing:master` | +| [List meeting participants](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipants) | `dashboard:read:list_meeting_participants:admin` | +| [List meeting participants](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipants) | `dashboard:read:list_meeting_participants:master` | +| [Get chat metrics](/docs/api/rest/reference/account/methods/#operation/dashboardChat) | `dashboard:read:chat:admin` | +| [Get chat metrics](/docs/api/rest/reference/account/ma/#operation/dashboardChat) | `dashboard:read:chat:master` | +| [Get meeting sharing/recording details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantShare) | `dashboard:read:meeting_sharing:admin` | +| [Get meeting sharing/recording details](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantShare) | `dashboard:read:meeting_sharing:master` | +| [Get meeting participant QoS](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingParticipantQOS) | `dashboard:read:meeting_participant_qos:admin` | +| [Get meeting participant QoS](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingParticipantQOS) | `dashboard:read:meeting_participant_qos:master` | +| [Get meeting details](/docs/api/rest/reference/account/methods/#operation/dashboardMeetingDetail) | `dashboard:read:meeting:admin` | +| [Get meeting details](/docs/api/rest/reference/account/ma/#operation/dashboardMeetingDetail) | `dashboard:read:meeting:master` | + +### Data Requests + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get download link for data access request file](/docs/api/rest/reference/account/methods/#operation/DownloadfilesfromDataRequest) | `data_request:read:download:admin` | +| [Cancel data deletion request](/docs/api/rest/reference/account/methods/#operation/CancelDataRequest) | `data_request:delete:request:admin` | +| [List downloadable files for export data request](/docs/api/rest/reference/account/methods/#operation/GetDownloadableFilesforDataRequest) | `data_request:read:download:admin` | +| [List data request history](/docs/api/rest/reference/account/methods/#operation/GetDataRequestsHistory) | `data_request:read:history:admin` | +| [Create data (export/deletion) request](/docs/api/rest/reference/account/methods/#operation/CreateDataAccessRequest) | `data_request:write:request:admin` | + +### Information Barriers + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove an Information Barrier policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersDelete) | `information_barrier:delete:policy:admin` | +| [Remove an Information Barrier policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersDelete) | `information_barrier:delete:policy:master` | +| [Update an Information Barriers policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersUpdate) | `information_barrier:update:policy:admin` | +| [Update an Information Barriers policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersUpdate) | `information_barrier:update:policy:master` | +| [Get an Information Barrier policy by ID](/docs/api/rest/reference/account/methods/#operation/InformationBarriersGet) | `information_barrier:read:policy:admin` | +| [Get an Information Barrier policy by ID](/docs/api/rest/reference/account/ma/#operation/InformationBarriersGet) | `information_barrier:read:policy:master` | +| [List information Barrier policies](/docs/api/rest/reference/account/methods/#operation/InformationBarriersList) | `information_barrier:read:list_policies:admin` | +| [List information Barrier policies](/docs/api/rest/reference/account/ma/#operation/InformationBarriersList) | `information_barrier:read:list_policies:master` | +| [Create an Information Barrier policy](/docs/api/rest/reference/account/methods/#operation/InformationBarriersCreate) | `information_barrier:write:policy:admin` | +| [Create an Information Barrier policy](/docs/api/rest/reference/account/ma/#operation/InformationBarriersCreate) | `information_barrier:write:policy:master` | + +### Roles + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign a role](/docs/api/rest/reference/account/methods/#operation/roleMemberDelete) | `role:delete:member`, `role:delete:member:admin` | +| [Unassign a role](/docs/api/rest/reference/account/ma/#operation/roleMemberDelete) | `role:delete:member:master` | +| [List roles](/docs/api/rest/reference/account/methods/#operation/roles) | `role:read:list_roles`, `role:read:list_roles:admin` | +| [List roles](/docs/api/rest/reference/account/ma/#operation/roles) | `role:read:list_roles:master` | +| [Update role information](/docs/api/rest/reference/account/methods/#operation/updateRole) | `role:update:role`, `role:update:role:admin` | +| [Update role information](/docs/api/rest/reference/account/ma/#operation/updateRole) | `role:update:role:master` | +| [Delete a role](/docs/api/rest/reference/account/methods/#operation/deleteRole) | `role:delete:role`, `role:delete:role:admin` | +| [Delete a role](/docs/api/rest/reference/account/ma/#operation/deleteRole) | `role:delete:role:master` | +| [Get role information](/docs/api/rest/reference/account/methods/#operation/getRoleInformation) | `role:read:role`, `role:read:role:admin` | +| [Get role information](/docs/api/rest/reference/account/ma/#operation/getRoleInformation) | `role:read:role:master` | +| [List members in a role](/docs/api/rest/reference/account/methods/#operation/roleMembers) | `role:read:list_members`, `role:read:list_members:admin` | +| [List members in a role](/docs/api/rest/reference/account/ma/#operation/roleMembers) | `role:read:list_members:master` | +| [Create a role](/docs/api/rest/reference/account/methods/#operation/createRole) | `role:write:role`, `role:write:role:admin` | +| [Create a role](/docs/api/rest/reference/account/ma/#operation/createRole) | `role:write:role:master` | +| [Assign a role](/docs/api/rest/reference/account/methods/#operation/AddRoleMembers) | `role:write:member`, `role:write:member:admin` | +| [Assign a role](/docs/api/rest/reference/account/ma/#operation/AddRoleMembers) | `role:write:member:master` | + +### Survey Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get survey info](/docs/api/rest/reference/account/methods/#operation/getSurveyInfo) | `survey_management:read:survey:admin` | +| [Get survey info](/docs/api/rest/reference/account/ma/#operation/getSurveyInfo) | `survey_management:read:survey:master` | +| [Get survey instances](/docs/api/rest/reference/account/methods/#operation/getSurveyInstancesInfo) | `survey_management:read:list_survey_instances:admin` | +| [Get survey instances](/docs/api/rest/reference/account/ma/#operation/getSurveyInstancesInfo) | `survey_management:read:list_survey_instances:master` | +| [Get survey answers](/docs/api/rest/reference/account/methods/#operation/getSurveyAnswers) | `survey_management:read:list_survey_answers:admin` | +| [Get survey answers](/docs/api/rest/reference/account/ma/#operation/getSurveyAnswers) | `survey_management:read:list_survey_answers:master` | +| [Get surveys](/docs/api/rest/reference/account/methods/#operation/getAccountSurveys) | `survey_management:read:list_surveys:admin` | +| [Get surveys](/docs/api/rest/reference/account/ma/#operation/getAccountSurveys) | `survey_management:read:list_surveys:master` | + +## Zoom Auto Dialer + + +### Prospect Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Next OAuth error messages](/docs/integrations/oauth-error-messages/) | `zoom_auto_dialer:update:prospects:admin` | + +## Zoom Billing + + +### Billing + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Cancel a base plan](/docs/api/rest/reference/billing/ma/#operation/CancelABasePlan) | `billing:update:cancel_plan:master` | +| [Download subaccounts' billing invoice reports](/docs/api/rest/reference/billing/ma/#operation/downloadBillingInvoiceReport) | `billing:read:download_invoice_reports:master` | +| [Download an invoice file](/docs/api/rest/reference/billing/ma/#operation/downloadInvoicePDF) | `billing:read:download_invoice_file:master` | +| [Get account plan information](/docs/api/rest/reference/billing/ma/#operation/GetAccountPlanInformation) | `billing:read:plan_information:master`, `billing:read:plan_information:admin` | +| [Update billing information](/docs/api/rest/reference/billing/ma/#operation/UpdateBillingInformation) | `billing:update:billing_information:master` | +| [List upcoming renewal accounts](/docs/api/rest/reference/billing/ma/#operation/Getupcomingrenewalaccounts) | `billing:read:list_upcoming_renewal_accounts:master` | +| [Update a base plan](/docs/api/rest/reference/billing/ma/#operation/UpdateABasePlan) | `billing:update:plan:master` | +| [Get invoice details](/docs/api/rest/reference/billing/ma/#operation/GetInvoiceDetails) | `billing:read:invoice_details:master`, `billing:read:invoice_details:admin` | +| [Update an account's additional plan](/docs/api/rest/reference/billing/ma/#operation/UpdateAnAccount'sAdditionalPlan) | `billing:update:additional_plans:master` | +| [Get billing information](/docs/api/rest/reference/billing/ma/#operation/GetBillingInformation) | `billing:read:billing_information:master`, `billing:read:billing_information:admin` | +| [List billing invoices](/docs/api/rest/reference/billing/ma/#operation/ListBillingInvoices) | `billing:read:list_invoices:master`, `billing:read:list_invoices:admin` | +| [Get plan usage](/docs/api/rest/reference/billing/ma/#operation/GetPlanUsage) | `billing:read:plan_usage:master`, `billing:read:plan_usage:admin` | +| [Subscribe subaccount to an additional plan](/docs/api/rest/reference/billing/ma/#operation/SubscribeAccountToAnAdditionalPlan) | `billing:write:subscribe_additional_plans:master` | +| [Cancel additional plans](/docs/api/rest/reference/billing/ma/#operation/CancelAdditionalPlans) | `billing:update:cancel_additional_plans:master` | +| [Download an invoice file (v2)](/docs/api/rest/reference/billing/ma/#operation/downloadInvoicePDFFile) | `billing:read:download_invoice_file:master` | +| [Delete subaccounts' billing invoice report](/docs/api/rest/reference/billing/ma/#operation/deleteBillingInvoiceReport) | `billing:delete:invoice_report:master` | +| [Subscribe an account to a plan](/docs/api/rest/reference/billing/ma/#operation/SubscribeAccountToAPlan) | `billing:write:subscribe_plan:master` | + +## Zoom Calendar + + +### acl + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a new ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertacl) | `calendar:write:acl`, `calendar:write:acl:admin` | +| [Delete an existing ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteacl) | `calendar:delete:acl`, `calendar:delete:acl:admin` | +| [Update the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchacl) | `calendar:update:acl`, `calendar:update:acl:admin` | +| [List ACL rules of specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listacl) | `calendar:read:list_acl`, `calendar:read:list_acl:admin` | +| [Get the specified ACL rule](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getacl) | `calendar:read:acl`, `calendar:read:acl:admin` | + +### calendar list + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Insert an existing calendar to the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/InsertcalendarList) | `calendar:write:calendar_list` | +| [Delete an existing calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/DeletecalendarList) | `calendar:delete:calendar_list` | +| [List the calendars in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/ListcalendarList) | `calendar:read:list_calendar_lists` | +| [Update an existing calendar in the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/PatchcalendarList) | `calendar:update:calendar_list` | +| [Get a specified calendar from the user's own calendarList](/docs/api/rest/reference/zoom-calendar/methods/#operation/GetcalendarList) | `calendar:read:calendar_list` | + +### calendars + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcalendar) | `calendar:read:calendar`, `calendar:read:calendar:admin` | +| [Delete a calendar owned by a user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deletecalendar) | `calendar:delete:calendar`, `calendar:delete:calendar:admin` | +| [Create a new secondary calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertcalendar) | `calendar:write:calendar`, `calendar:write:calendar:admin` | +| [Update the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchcalendar) | `calendar:update:calendar`, `calendar:update:calendar:admin` | + +### colors + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the color definitions for calendars and events](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getcolor) | `calendar:read:color`, `calendar:read:color:admin` | + +### events + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Quick add an event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Quickaddevent) | `calendar:write:quick_add_event`, `calendar:write:quick_add_event:admin` | +| [Move the specified event from a calendar to another specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Moveevent) | `calendar:write:move_event`, `calendar:write:move_event:admin` | +| [Get the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getevent) | `calendar:read:event`, `calendar:read:event:admin` | +| [Update the specified event on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchevent) | `calendar:update:event`, `calendar:update:event:admin` | +| [Import event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Importevent) | `calendar:write:import_event`, `calendar:write:import_event:admin` | +| [List events on the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listevent) | `calendar:read:list_events`, `calendar:read:list_events:admin` | +| [Insert a new event to the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Insertevent) | `calendar:write:event`, `calendar:write:event:admin` | +| [List all instances of the specified recurring event](/docs/api/rest/reference/zoom-calendar/methods/#operation/Instanceevent) | `calendar:read:instance_event`, `calendar:read:instance_event:admin` | +| [Delete an existing event from the specified calendar](/docs/api/rest/reference/zoom-calendar/methods/#operation/Deleteevent) | `calendar:delete:event`, `calendar:delete:event:admin` | + +### freebusy + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Query freebusy information for a set of calendars](/docs/api/rest/reference/zoom-calendar/methods/#operation/Queryfreebusy) | `calendar:read:list_events`, `calendar:read:list_events:admin` | + +### settings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Getsetting) | `calendar:read:setting` | +| [List all user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Listsettings) | `calendar:read:list_settings` | +| [Patch the specified user calendar settings of the authenticated user](/docs/api/rest/reference/zoom-calendar/methods/#operation/Patchsetting) | `calendar:update:setting`, `calendar:update:setting:admin` | + +## Zoom Clips + + +### Clips + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List all clips](/docs/api/rest/reference/clips/methods/#operation/GetUserClips) | `clips:read:list_user_clips`, `clips:read:list_user_clips:admin` | +| [List all clips](/docs/api/rest/reference/clips/ma/#operation/GetUserClips) | `clips:read:list_user_clips:master` | + +### Collaborator + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove the collaborator from a clip](/docs/api/rest/reference/clips/methods/#operation/DeleteCollaborator) | `clips:delete:collaborators`, `clips:delete:collaborators:admin` | +| [Get collaborators of a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipCollaborators) | `clips:read:list_collaborator`, `clips:read:list_collaborator:admin` | + +### Comment + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List clip comments](/docs/api/rest/reference/clips/methods/#operation/Listclipcomments) | `clips:read:list_comments`, `clips:read:list_comments:admin` | +| [Delete a comment](/docs/api/rest/reference/clips/methods/#operation/Deleteacomment) | `clips:delete:comment`, `clips:delete:comment:admin` | + +### Download + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Download a clip](/docs/api/rest/reference/clips/methods/#operation/downloadClip) | `clips:read:download_clip`, `clips:read:download_clip:admin` | + +### Single + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a clip(soft delete)](/docs/api/rest/reference/clips/methods/#operation/DeleteClip) | `clips:delete:clip`, `clips:delete:clip:admin` | +| [Get a clip](/docs/api/rest/reference/clips/methods/#operation/GetClipById) | `clips:read:clip`, `clips:read:clip:admin` | + +### Transfer + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Transfer task status check](/docs/api/rest/reference/clips/methods/#operation/Transfertaskstatuscheck) | `clips:read:transfer_task_status:admin` | +| [Transfer clips owner](/docs/api/rest/reference/clips/methods/#operation/Transferclipsowner) | `clips:write:transfer_owner:admin` | + +## Zoom Commerce + + +### Account Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the list of all accounts associated with a Zoom Partner/Sub-Reseller, by the account type](/docs/api/rest/reference/commerce/methods/#operation/getAllAccounts) | `zoom_commerce:read:account:admin` | +| [Get the account details for a Zoom Partner/Subreseller/End Customer](/docs/api/rest/reference/commerce/methods/#operation/getAccountDetails) | `zoom_commerce:read:account:admin` | +| [Create an end customer account](/docs/api/rest/reference/commerce/methods/#operation/createAccount) | `zoom_commerce:write:sub_account:admin` | +| [Add contacts to an existing end customer or your own account.](/docs/api/rest/reference/commerce/methods/#operation/addAccountContact) | `zoom_commerce:update:account:admin` | + +### Billing + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets the PDF document for the billing document ID](/docs/api/rest/reference/commerce/methods/#operation/downloadBillingDoc) | `zoom_commerce:read:billing_documents:admin` | +| [Gets all billing documents for a distributor or a reseller](/docs/api/rest/reference/commerce/methods/#operation/getAllBillingDocs) | `zoom_commerce:read:billing_documents:admin` | +| [Get detailed information about a specific invoice for a distributor or a reseller](/docs/api/rest/reference/commerce/methods/#operation/getInvoiceDetail) | `zoom_commerce:read:billing_documents:admin` | + +### Deal Registration + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets details of a deal registration by the deal registration](/docs/api/rest/reference/commerce/methods/#operation/getDealRegDetails) | `zoom_commerce:read:deal_registration:admin` | +| [Retrieves all valid Zoom Campaigns which a deal registration can be associated with.](/docs/api/rest/reference/commerce/methods/#operation/getCampaigns) | `zoom_commerce:read:deal_registration:admin` | +| [Creates a new deal registration for a partner](/docs/api/rest/reference/commerce/methods/#operation/createDealReg) | `zoom_commerce:write:deal_registration:admin` | +| [Updates an existing deal registration](/docs/api/rest/reference/commerce/methods/#operation/Updatesanexistingdealregistration) | `zoom_commerce:write:deal_registration:admin` | +| [Gets all valid Deal Registrations for a partner](/docs/api/rest/reference/commerce/methods/#operation/getAllDealRegs) | `zoom_commerce:read:deal_registration:admin` | + +### Order + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Preview delta order metrics and subscriptions in an order](/docs/api/rest/reference/commerce/methods/#operation/createOrderPreview) | `zoom_commerce:write:order:admin` | +| [Get order details by order reference ID](/docs/api/rest/reference/commerce/methods/#operation/getOrderDetails) | `zoom_commerce:read:order:admin` | +| [Create a subscription order for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/createOrder) | `zoom_commerce:write:order:admin` | +| [Gets all orders for a Zoom partner.](/docs/api/rest/reference/commerce/methods/#operation/getAllOrders) | `zoom_commerce:read:order:admin` | + +### Platform + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets details of all files associated with a quote or deal registration](/docs/api/rest/reference/commerce/methods/#operation/allFileDetails) | `zoom_commerce:read:file:admin` | +| [Download a file associated with a quote or deal registration.](/docs/api/rest/reference/commerce/methods/#operation/downloadFile.) | `zoom_commerce:read:file:admin` | +| [Upload an attachment pdf file in context of a deal registration or quote](/docs/api/rest/reference/commerce/methods/#operation/uploadFile) | `zoom_commerce:write:file:admin` | + +### Product Catalog + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets the details for a Zoom product or offer.](/docs/api/rest/reference/commerce/methods/#operation/getOfferDetail) | `zoom_commerce:read:product_catalog:admin` | +| [Gets the pricebook in a downloadable file](/docs/api/rest/reference/commerce/methods/#operation/downloadPricebook) | `zoom_commerce:read:product_catalog:admin` | +| [Gets Zoom Product Catalog for a Zoom Partner](/docs/api/rest/reference/commerce/methods/#operation/getOffers) | `zoom_commerce:read:product_catalog:admin` | + +### Quote + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets all quotes for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/getAllQuotes) | `zoom_commerce:read:quote:admin` | +| [Preview delta quote metrics and subscriptions in a quote](/docs/api/rest/reference/commerce/methods/#operation/createQuotePreview) | `zoom_commerce:write:quote:admin` | +| [Submits a subscription quote for provisioning](/docs/api/rest/reference/commerce/methods/#operation/provisionQuote) | `zoom_commerce:write:quote:admin` | +| [Update a subscription quote for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/updateQuote) | `zoom_commerce:write:quote:admin` | +| [Create a subscription quote for a Zoom Partner](/docs/api/rest/reference/commerce/methods/#operation/createQuote) | `zoom_commerce:write:quote:admin` | +| [Get quote details by quote reference ID](/docs/api/rest/reference/commerce/methods/#operation/getQuoteDetails) | `zoom_commerce:read:quote:admin` | + +### Subscription + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Gets subscription details for a given subscription number](/docs/api/rest/reference/commerce/methods/#operation/getSubscriptionDetails) | `zoom_commerce:read:subscription:admin` | +| [Get trial details for an end customer by their Zoom account number or the trial ID](/docs/api/rest/reference/commerce/methods/#operation/getTrialDetails) | `zoom_commerce:read:subscription:admin` | +| [Gets paid subscriptions for a Zoom partner.](/docs/api/rest/reference/commerce/methods/#operation/getAllSubscriptions) | `zoom_commerce:read:subscription:admin` | +| [Get trial subscriptions for a Zoom partner](/docs/api/rest/reference/commerce/methods/#operation/getAllTrialSubscriptions) | `zoom_commerce:read:subscription:admin` | +| [Gets subscription changes/versions for a given subscription number.](/docs/api/rest/reference/commerce/methods/#operation/getSubscriptionVersions) | `zoom_commerce:read:subscription:admin` | + +## Zoom Events + + +### Attendee Actions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update event attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateEventAttendeeActions) | `zoom_events:update:batch_attendee_actions`, `zoom_events:update:batch_attendee_actions:admin` | +| [List event attendee actions](/docs/api/rest/reference/event/methods/#operation/ListEventAttendeeActions) | `zoom_events:read:list_attendee_actions`, `zoom_events:read:list_attendee_actions:admin` | +| [List session attendee actions](/docs/api/rest/reference/event/methods/#operation/ListSessionAttendeeActions) | `zoom_events:read:list_session_attendee_actions`, `zoom_events:read:list_session_attendee_actions:admin` | +| [Update session attendee actions](/docs/api/rest/reference/event/methods/#operation/UpdateSessionAttendeeActions) | `zoom_events:update:batch_session_attendee_actions`, `zoom_events:update:batch_session_attendee_actions:admin` | + +### Co Editors + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add or remove event co-editors](/docs/api/rest/reference/event/methods/#operation/coeditoractions) | `zoom_events:write:coeditor`, `zoom_events:write:coeditor:admin` | +| [List coeditors](/docs/api/rest/reference/event/methods/#operation/getCoEditors) | `zoom_events:read:list_coeditors`, `zoom_events:read:list_coeditors:admin` | + +### Emails + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List event emails sent status](/docs/api/rest/reference/event/methods/#operation/listEmailSentStatuses) | `zoom_events:read:list_emails_status`, `zoom_events:read:list_emails_status:admin` | +| [List event email types](/docs/api/rest/reference/event/methods/#operation/listEmailTypes) | `zoom_events:read:list_email_types`, `zoom_events:read:list_email_types:admin` | + +### Event Access + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List event access links](/docs/api/rest/reference/event/methods/#operation/getEventAccessLinks) | `zoom_events:read:list_access_links`, `zoom_events:read:list_access_links:admin` | +| [Get event access link](/docs/api/rest/reference/event/methods/#operation/GetEventAccessLink) | `zoom_events:read:access_links`, `zoom_events:read:access_links:admin` | +| [Update event access](/docs/api/rest/reference/event/methods/#operation/updateEventAccess) | `zoom_events:update:access_links`, `zoom_events:update:access_links:admin` | +| [Delete event access link](/docs/api/rest/reference/event/methods/#operation/deleteEventAccessLink) | `zoom_events:delete:access_links`, `zoom_events:delete:access_links:admin` | +| [Create event access link](/docs/api/rest/reference/event/methods/#operation/createEventAccessLink) | `zoom_events:write:access_links`, `zoom_events:write:access_links:admin` | + +### Events + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List events](/docs/api/rest/reference/event/methods/#operation/getEvents) | `zoom_events:read:list_events`, `zoom_events:read:list_events:admin` | +| [Get an event](/docs/api/rest/reference/event/methods/#operation/getEventInfo) | `zoom_events:read:event`, `zoom_events:read:event:admin` | +| [Delete an event](/docs/api/rest/reference/event/methods/#operation/deleteEvent) | `zoom_events:delete:event`, `zoom_events:delete:event:admin` | +| [Event actions](/docs/api/rest/reference/event/methods/#operation/EventActions) | `zoom_events:write:event`, `zoom_events:write:event:admin` | +| [Update an event](/docs/api/rest/reference/event/methods/#operation/updateEvent) | `zoom_events:update:event`, `zoom_events:update:event:admin` | +| [Create an event](/docs/api/rest/reference/event/methods/#operation/createEvent) | `zoom_events:write:event`, `zoom_events:write:event:admin` | + +### Exhibitors + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create an exhibitor](/docs/api/rest/reference/event/methods/#operation/createExhibitor) | `zoom_events:write:exhibitor`, `zoom_events:write:exhibitor:admin` | +| [List exhibitors](/docs/api/rest/reference/event/methods/#operation/getExhibitors) | `zoom_events:read:list_exhibitors`, `zoom_events:read:list_exhibitors:admin` | +| [Update exhibitor for an event](/docs/api/rest/reference/event/methods/#operation/updateExhibitor) | `zoom_events:update:exhibitor`, `zoom_events:update:exhibitor:admin` | +| [Delete an exhibitor](/docs/api/rest/reference/event/methods/#operation/deleteExhibitor) | `zoom_events:delete:exhibitor`, `zoom_events:delete:exhibitor:admin` | +| [List sponsor tiers](/docs/api/rest/reference/event/methods/#operation/ListSponsorTiers) | `zoom_events:read:list_sponsor_tiers`, `zoom_events:read:list_sponsor_tiers:admin` | +| [Get an exhibitor](/docs/api/rest/reference/event/methods/#operation/getExhibitorInfo) | `zoom_events:read:exhibitor`, `zoom_events:read:exhibitor:admin` | + +### Files + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Initiate and complete the multipart file upload](/docs/api/rest/reference/event/methods/#operation/initiateAndCompleteAEventMultipartUpload.) | `zoom_events:write:file_upload`, `zoom_events:write:file_upload:admin` | +| [Upload events multipart files](/docs/api/rest/reference/event/methods/#operation/uploadMultipartEventFile) | `zoom_events:write:file_upload`, `zoom_events:write:file_upload:admin` | +| [Upload events file](/docs/api/rest/reference/event/methods/#operation/uploadEventFile) | `zoom_events:write:file_upload`, `zoom_events:write:file_upload:admin` | + +### Hubs + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Creates a new hub host](/docs/api/rest/reference/event/methods/#operation/createHubHost) | `zoom_events:write:hub_host`, `zoom_events:write:hub_host:admin` | +| [List hub Hosts](/docs/api/rest/reference/event/methods/#operation/gethubhostList) | `zoom_events:read:list_hub_hosts`, `zoom_events:read:list_hub_hosts:admin` | +| [List hub videos](/docs/api/rest/reference/event/methods/#operation/ListHubVideos) | `zoom_events:read:list_hub_videos`, `zoom_events:read:list_hub_videos:admin` | +| [Remove hub host](/docs/api/rest/reference/event/methods/#operation/deleteHubHost) | `zoom_events:delete:hub_host`, `zoom_events:delete:hub_host:admin` | +| [List hubs](/docs/api/rest/reference/event/methods/#operation/getHubList) | `zoom_events:read:list_hubs`, `zoom_events:read:list_hubs:admin` | + +### Registrants + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List registrants](/docs/api/rest/reference/event/methods/#operation/getRegistrants) | `zoom_events:read:list_registrants`, `zoom_events:read:list_registrants:admin` | +| [List session attendees](/docs/api/rest/reference/event/methods/#operation/getSessionAttendeeList) | `zoom_events:read:list_session_attendees`, `zoom_events:read:list_session_attendees:admin` | + +### Reports + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get VOD channel registration report](/docs/api/rest/reference/event/methods/#operation/VodChannelReigistration) | `zoom_events:read:vod_registrations`, `zoom_events:read:vod_registrations:admin` | +| [Get event survey report](/docs/api/rest/reference/event/methods/#operation/EventSurveyReportApi) | `zoom_events:read:list_session_surveys`, `zoom_events:read:list_session_surveys:admin` | +| [Get session attendance report](/docs/api/rest/reference/event/methods/#operation/SessionAttendanceReport) | `zoom_events:read:session_attendance`, `zoom_events:read:session_attendance:admin` | +| [Get chat transcripts report](/docs/api/rest/reference/event/methods/#operation/ChatTranscriptsReport) | `zoom_events:read:chat_transcripts`, `zoom_events:read:chat_transcripts:admin` | +| [Get custom report](/docs/api/rest/reference/event/methods/#operation/getCustomEventReport) | `zoom_events:read:custom_report`, `zoom_events:read:custom_report:admin` | +| [Get event registrations report](/docs/api/rest/reference/event/methods/#operation/EventRegistrationsReport) | `zoom_events:read:event_registration`, `zoom_events:read:event_registration:admin` | +| [Get event attendance (Live or Lobby) report](/docs/api/rest/reference/event/methods/#operation/EventAttendanceReport) | `zoom_events:read:event_attendance`, `zoom_events:read:event_attendance:admin` | + +### Sessions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List sessions](/docs/api/rest/reference/event/methods/#operation/getEventSessionList) | `zoom_events:read:list_sessions`, `zoom_events:read:list_sessions:admin` | +| [List session reservations](/docs/api/rest/reference/event/methods/#operation/ListSessionReservations) | `zoom_events:read:list_session_reservations`, `zoom_events:read:list_session_reservations:admin` | +| [Add session reservations](/docs/api/rest/reference/event/methods/#operation/AddSessionReservations) | `zoom_events:write:session_reservations`, `zoom_events:write:session_reservations:admin` | +| [Update a session](/docs/api/rest/reference/event/methods/#operation/updateEventSession) | `zoom_events:update:session`, `zoom_events:update:session:admin` | +| [Get the session information](/docs/api/rest/reference/event/methods/#operation/getEventSessionInfo) | `zoom_events:read:session`, `zoom_events:read:session:admin` | +| [Create a session](/docs/api/rest/reference/event/methods/#operation/createEventSession) | `zoom_events:write:session`, `zoom_events:write:session:admin` | +| [List session polls](/docs/api/rest/reference/event/methods/#operation/getSessionPolls) | `zoom_events:read:list_session_polls`, `zoom_events:read:list_session_polls:admin` | +| [Get session livestream configuration](/docs/api/rest/reference/event/methods/#operation/getSessionLivestreamConfiguration) | `zoom_events:read:session_livestream_config`, `zoom_events:read:session_livestream_config:admin` | +| [Delete session reservations](/docs/api/rest/reference/event/methods/#operation/DeleteSessionReservations) | `zoom_events:delete:session_reservations`, `zoom_events:delete:session_reservations:admin` | +| [Update session livestream configuration](/docs/api/rest/reference/event/methods/#operation/UpdateSessionLivestreamConfiguration) | `zoom_events:update:session_livestream_config`, `zoom_events:update:session_livestream_config:admin` | +| [List session interpreters](/docs/api/rest/reference/event/methods/#operation/getSessionInterpreterList) | `zoom_events:read:list_session_interpreters`, `zoom_events:read:list_session_interpreters:admin` | +| [Delete a session](/docs/api/rest/reference/event/methods/#operation/deleteEventSession) | `zoom_events:delete:session`, `zoom_events:delete:session:admin` | +| [Create or update session polls](/docs/api/rest/reference/event/methods/#operation/updateSessionPolls) | `zoom_events:update:session_poll`, `zoom_events:update:session_poll:admin` | +| [Get ticket session join token by Event ID and Session ID](/docs/api/rest/reference/event/methods/#operation/getSessionJoinToken) | `zoom_events:read:session_token`, `zoom_events:read:session_token:admin` | +| [Create or update session interpreters](/docs/api/rest/reference/event/methods/#operation/updateSessionInterpreters) | `zoom_events:update:session_interpreter`, `zoom_events:update:session_interpreter:admin` | + +### Speakers + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List speakers](/docs/api/rest/reference/event/methods/#operation/getSpeakers) | `zoom_events:read:list_speakers`, `zoom_events:read:list_speakers:admin` | +| [Get a speaker](/docs/api/rest/reference/event/methods/#operation/getSpeaker) | `zoom_events:read:speaker`, `zoom_events:read:speaker:admin` | +| [Create a speaker](/docs/api/rest/reference/event/methods/#operation/createSpeaker) | `zoom_events:write:speaker`, `zoom_events:write:speaker:admin` | +| [Delete a speaker](/docs/api/rest/reference/event/methods/#operation/deleteSpeaker) | `zoom_events:delete:speaker`, `zoom_events:delete:speaker:admin` | +| [Update a speaker](/docs/api/rest/reference/event/methods/#operation/updateSpeaker) | `zoom_events:update:speaker`, `zoom_events:update:speaker:admin` | + +### Ticket Types + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List ticket types](/docs/api/rest/reference/event/methods/#operation/getEventTicketTypes) | `zoom_events:read:list_ticket_types`, `zoom_events:read:list_ticket_types:admin` | +| [Update registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForTicketType) | `zoom_events:update:registraion_question`, `zoom_events:update:registraion_question:admin` | +| [Delete a ticket type](/docs/api/rest/reference/event/methods/#operation/deleteEventTicketType) | `zoom_events:delete:ticket_type`, `zoom_events:delete:ticket_type:admin` | +| [Update ticket type for an event](/docs/api/rest/reference/event/methods/#operation/updateTicketType) | `zoom_events:update:ticket_type`, `zoom_events:update:ticket_type:admin` | +| [List registration questions for ticket type](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForTicketType) | `zoom_events:read:list_registration_questions`, `zoom_events:read:list_registration_questions:admin` | +| [Create an event ticket type](/docs/api/rest/reference/event/methods/#operation/createTicketType) | `zoom_events:write:ticket_type`, `zoom_events:write:ticket_type:admin` | +| [Update registration questions for an event](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForEvent) | `zoom_events:update:registraion_question`, `zoom_events:update:registraion_question:admin` | +| [List registration questions for an event](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForEvent) | `zoom_events:read:list_registration_questions`, `zoom_events:read:list_registration_questions:admin` | + +### Tickets + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update ticket](/docs/api/rest/reference/event/methods/#operation/Updateticket) | `zoom_events:write:ticket`, `zoom_events:write:ticket:admin` | +| [Create tickets](/docs/api/rest/reference/event/methods/#operation/createTickets) | `zoom_events:write:ticket`, `zoom_events:write:ticket:admin` | +| [Get a ticket](/docs/api/rest/reference/event/methods/#operation/getTicketDetails) | `zoom_events:read:ticket`, `zoom_events:read:ticket:admin` | +| [Delete a ticket](/docs/api/rest/reference/event/methods/#operation/deleteTicket) | `zoom_events:delete:ticket`, `zoom_events:delete:ticket:admin` | +| [List tickets](/docs/api/rest/reference/event/methods/#operation/getTickets) | `zoom_events:read:list_tickets`, `zoom_events:read:list_tickets:admin` | + +### Video On-Demand + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [VOD channel actions](/docs/api/rest/reference/event/methods/#operation/vodChannelActions) | `zoom_events:update:vod_channel`, `zoom_events:update:vod_channel:admin` | +| [List VOD channel videos](/docs/api/rest/reference/event/methods/#operation/ListVODChannelVideos) | `zoom_events:read:list_vod_channel_videos`, `zoom_events:read:list_vod_channel_videos:admin` | +| [Delete VOD Channel](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannel) | `zoom_events:delete:vod_channel`, `zoom_events:delete:vod_channel:admin` | +| [Create VOD channel](/docs/api/rest/reference/event/methods/#operation/createVodChannel) | `zoom_events:write:vod_channel`, `zoom_events:write:vod_channel:admin` | +| [Delete VOD channel video](/docs/api/rest/reference/event/methods/#operation/DeleteVODChannelVideo) | `zoom_events:delete:vod_channel_videos`, `zoom_events:delete:vod_channel_videos:admin` | +| [Update VOD channel](/docs/api/rest/reference/event/methods/#operation/UpdateVideoChannel) | `zoom_events:update:vod_channel`, `zoom_events:update:vod_channel:admin` | +| [Get VOD channel details](/docs/api/rest/reference/event/methods/#operation/getVODChannelDetail) | `zoom_events:read:vod_channel`, `zoom_events:read:vod_channel:admin` | +| [List channels](/docs/api/rest/reference/event/methods/#operation/getVODChannels) | `zoom_events:read:list_all_vod_channels`, `zoom_events:read:list_all_vod_channels:admin` | +| [Add VOD channel videos](/docs/api/rest/reference/event/methods/#operation/AddVODChannelVideos) | `zoom_events:write:vod_channel_videos`, `zoom_events:write:vod_channel_videos:admin` | + +### Video On-Demand Registrations + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List VOD Registration](/docs/api/rest/reference/event/methods/#operation/ListVODRegistration) | `zoom_events:read:vod_registrations`, `zoom_events:read:vod_registrations:admin` | +| [Get VOD Registration Questions](/docs/api/rest/reference/event/methods/#operation/getRegistrationQuestionsForVODChannel) | `zoom_events:read:vod_registration_questions`, `zoom_events:read:vod_registration_questions:admin` | +| [update VOD channel registration questions](/docs/api/rest/reference/event/methods/#operation/updateRegistrationQuestionsForVODchannel) | `zoom_events:update:vod_registration_questions`, `zoom_events:update:vod_registration_questions:admin` | +| [VOD channel registration](/docs/api/rest/reference/event/methods/#operation/VODTicketRegistration) | `zoom_events:write:vod_registration`, `zoom_events:write:vod_registration:admin` | + +## Zoom Mail + + +### Drafts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_draft_email) | `email:update:draft`, `email:update:draft:admin` | +| [List emails from draft folder](/docs/api/rest/reference/zoom-mail/methods/#operation/list_draft_emails) | `email:read:list_drafts`, `email:read:list_drafts:admin` | +| [Get the specified draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_draft_email) | `email:read:draft`, `email:read:draft:admin` | +| [Send out a draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_draft_email) | `email:write:send_draft`, `email:write:send_draft:admin` | +| [Delete an existing draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_draft_email) | `email:delete:draft`, `email:delete:draft:admin` | +| [Create a new draft email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_draft_email) | `email:write:draft`, `email:write:draft:admin` | + +### History + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List history of events for mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_history) | `email:read:history`, `email:read:history:admin` | + +### Labels + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_label_in_mailbox) | `email:read:label`, `email:read:label:admin` | +| [Update the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/update_label_in_mailbox) | `email:update:label`, `email:update:label:admin` | +| [List labels in the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_labels_in_mailbox) | `email:read:list_labels`, `email:read:list_labels:admin` | +| [Create a new label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/create_label_in_mailbox) | `email:write:label`, `email:write:label:admin` | +| [Delete an existing label from mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_label_from_mailbox) | `email:delete:label`, `email:delete:label:admin` | +| [Patch the specified label in mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/patch_label_in_mailbox) | `email:update:label`, `email:update:label:admin` | + +### Mailbox + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the mailbox profile](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_profile) | `email:read:profile`, `email:read:profile:admin` | + +### Messages + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete an existing email](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email) | `email:delete:msg`, `email:delete:msg:admin` | +| [Move the specified email to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email) | `email:write:trash_msg`, `email:write:trash_msg:admin` | +| [Move the specified email out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email) | `email:write:untrash_msg`, `email:write:untrash_msg:admin` | +| [Update the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email) | `email:write:modify_msg`, `email:write:modify_msg:admin` | +| [List emails from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_emails) | `email:read:list_msgs`, `email:read:list_msgs:admin` | +| [Send out an email](/docs/api/rest/reference/zoom-mail/methods/#operation/send_email) | `email:write:send_msg`, `email:write:send_msg:admin` | +| [Batch delete the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_delete_emails) | `email:write:batch_delete_msgs`, `email:write:batch_delete_msgs:admin` | +| [Batch modify the specified emails](/docs/api/rest/reference/zoom-mail/methods/#operation/batch_modify_emails) | `email:write:batch_modify_msgs`, `email:write:batch_modify_msgs:admin` | +| [Create a new email](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email) | `email:write:msg`, `email:write:msg:admin` | +| [Get the specified email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email) | `email:read:msg`, `email:read:msg:admin` | + +### Messages.Attachments + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the specified attachment for an email](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_attachment) | `email:read:attachment`, `email:read:attachment:admin` | + +### Settings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/update_mailbox_vacation_response_setting) | `email:update:setting_vacation`, `email:update:setting_vacation:admin` | +| [Get mailbox vacation response setting](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mail_vacation_response_setting) | `email:read:setting_vacation`, `email:read:setting_vacation:admin` | + +### Settings.Delegates + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Revoke an existing delegate access from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/revoke_mailbox_delegate) | `email:delete:setting_delegate`, `email:delete:setting_delegate:admin` | +| [List delegates on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_mailbox_delegates) | `email:read:list_setting_delegates`, `email:read:list_setting_delegates:admin` | +| [Grant a new delegate access on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/grant_mailbox_delegate) | `email:write:setting_delegate`, `email:write:setting_delegate:admin` | +| [Get the specified delegate on the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/get_mailbox_delegate) | `email:read:setting_delegate`, `email:read:setting_delegate:admin` | + +### Settings.Filters + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_filter) | `email:read:setting_filter`, `email:read:setting_filter:admin` | +| [Delete the specified email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_filter) | `email:delete:setting_filter`, `email:delete:setting_filter:admin` | +| [Create an email filter](/docs/api/rest/reference/zoom-mail/methods/#operation/create_email_filter) | `email:write:setting_filter`, `email:write:setting_filter:admin` | +| [List email filters](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_filters) | `email:read:list_setting_filters`, `email:read:list_setting_filters:admin` | + +### Threads + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete an existing email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/delete_email_thread) | `email:delete:thread`, `email:delete:thread:admin` | +| [Get the specified email thread](/docs/api/rest/reference/zoom-mail/methods/#operation/get_email_thread) | `email:read:thread`, `email:read:thread:admin` | +| [Move the specified thread out of TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/untrash_email_thread) | `email:write:untrash_thread`, `email:write:untrash_thread:admin` | +| [Move the specified thread to TRASH folder](/docs/api/rest/reference/zoom-mail/methods/#operation/trash_email_thread) | `email:write:trash_thread`, `email:write:trash_thread:admin` | +| [List email threads from the mailbox](/docs/api/rest/reference/zoom-mail/methods/#operation/list_email_threads) | `email:read:list_threads`, `email:read:list_threads:admin` | +| [Update the specified thread](/docs/api/rest/reference/zoom-mail/methods/#operation/update_email_thread) | `email:write:thread`, `email:write:thread:admin` | + +## Zoom Meeting + + +### Archiving + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a meeting's archived files](/docs/api/rest/reference/zoom-api/methods/#operation/getArchivedFiles) | `archiving:read:archived_files:admin`, `archiving:read:archived_files` | +| [List archived files](/docs/api/rest/reference/zoom-api/methods/#operation/listArchivedFiles) | `archiving:read:list_archived_files:admin`, `archiving:read:list_archived_files:master` | +| [List archived files](/docs/api/rest/reference/zoom-api/ma/#operation/listArchivedFiles) | `archiving:read:list_archived_files:master` | +| [Get archived file statistics](/docs/api/rest/reference/zoom-api/methods/#operation/getArchivedFileStatistics) | `archiving:read:archived_file_statistics:admin` | +| [Get a meeting's archive token for local archiving](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLocalArchivingArchiveToken) | `meeting:read:local_archiving_token:admin` | +| [Update an archived file's auto-delete status](/docs/api/rest/reference/zoom-api/methods/#operation/updateArchivedFile) | `archiving:update:archived_file_auto_delete_status`, `archiving:update:archived_file_auto_delete_status:admin` | +| [Delete a meeting's archived files](/docs/api/rest/reference/zoom-api/methods/#operation/deleteArchivedFiles) | `archiving:delete:archived_files:admin` | + +### Cloud Recording + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a meeting or webinar recording's analytics details](/docs/api/rest/reference/zoom-api/methods/#operation/analytics_details) | `cloud_recording:read:recording_analytics_details`, `cloud_recording:read:recording_analytics_details:master`, `cloud_recording:read:recording_analytics_details:admin` | +| [Get a meeting or webinar recording's analytics details](/docs/api/rest/reference/zoom-api/ma/#operation/analytics_details) | `cloud_recording:read:recording_analytics_details:master` | +| [Get a meeting transcript](/docs/api/rest/reference/zoom-api/methods/#operation/GetMeetingTranscript) | `cloud_recording:read:meeting_transcript`, `cloud_recording:read:meeting_transcript:admin` | +| [Recover meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdate) | `cloud_recording:update:recover_meeting_recordings`, `cloud_recording:update:recover_meeting_recordings:master`, `cloud_recording:update:recover_meeting_recordings:admin` | +| [Recover meeting recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingStatusUpdate) | `cloud_recording:update:recover_meeting_recordings:master` | +| [Get meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingUpdate) | `cloud_recording:read:recording_settings`, `cloud_recording:read:recording_settings:admin`, `cloud_recording:read:recording_settings:master` | +| [Get meeting recording settings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingSettingUpdate) | `cloud_recording:read:recording_settings:master` | +| [Get registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantsQuestionsGet) | `cloud_recording:read:registration_questions`, `cloud_recording:read:registration_questions:master`, `cloud_recording:read:registration_questions:admin` | +| [Get registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/recordingRegistrantsQuestionsGet) | `cloud_recording:read:registration_questions:master` | +| [Create a recording registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantCreate) | `cloud_recording:write:recording_registrant`, `cloud_recording:write:recording_registrant:master`, `cloud_recording:write:recording_registrant:admin` | +| [Create a recording registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrantCreate) | `cloud_recording:write:recording_registrant:master` | +| [List an account's recordings](/docs/api/rest/reference/zoom-api/ma/#operation/ListRecordingsOfAnAccount) | `cloud_recording:read:list_account_recordings:master`, `cloud_recording:read:list_account_recordings:admin` | +| [List all recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingsList) | `cloud_recording:read:list_user_recordings`, `cloud_recording:read:list_user_recordings:master`, `cloud_recording:read:list_user_recordings:admin` | +| [List all recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingsList) | `cloud_recording:read:list_user_recordings:master` | +| [Update a registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrantStatus) | `cloud_recording:update:registrant_status`, `cloud_recording:update:registrant_status:master`, `cloud_recording:update:registrant_status:admin` | +| [Update a registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrantStatus) | `cloud_recording:update:registrant_status:master` | +| [Get a meeting or webinar recording's analytics summary](/docs/api/rest/reference/zoom-api/methods/#operation/analytics_summary) | `cloud_recording:read:recording_analytics_summary`, `cloud_recording:read:recording_analytics_summary:master`, `cloud_recording:read:recording_analytics_summary:admin` | +| [Get a meeting or webinar recording's analytics summary](/docs/api/rest/reference/zoom-api/ma/#operation/analytics_summary) | `cloud_recording:read:recording_analytics_summary:master` | +| [Recover a single recording](/docs/api/rest/reference/zoom-api/methods/#operation/recordingStatusUpdateOne) | `cloud_recording:update:recover_single_recording`, `cloud_recording:update:recover_single_recording:master`, `cloud_recording:update:recover_single_recording:admin` | +| [Recover a single recording](/docs/api/rest/reference/zoom-api/ma/#operation/recordingStatusUpdateOne) | `cloud_recording:update:recover_single_recording:master` | +| [List recording registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRecordingRegistrants) | `cloud_recording:read:list_recording_registrants`, `cloud_recording:read:list_recording_registrants:admin`, `cloud_recording:read:list_recording_registrants:master` | +| [List recording registrants](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRecordingRegistrants) | `cloud_recording:read:list_recording_registrants:master` | +| [Delete a recording file for a meeting or webinar](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDeleteOne) | `cloud_recording:delete:recording_file`, `cloud_recording:delete:recording_file:admin`, `cloud_recording:delete:recording_file:master` | +| [Delete a recording file for a meeting or webinar](/docs/api/rest/reference/zoom-api/ma/#operation/recordingDeleteOne) | `cloud_recording:delete:recording_file:master` | +| [Delete meeting or webinar recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingDelete) | `cloud_recording:delete:meeting_recording`, `cloud_recording:delete:meeting_recording:admin`, `cloud_recording:delete:meeting_recording:master` | +| [Delete meeting or webinar recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingDelete) | `cloud_recording:delete:meeting_recording:master` | +| [Update meeting recording settings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingSettingsUpdate) | `cloud_recording:update:recording_settings`, `cloud_recording:update:recording_settings:master`, `cloud_recording:update:recording_settings:admin` | +| [Update meeting recording settings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingSettingsUpdate) | `cloud_recording:update:recording_settings:master` | +| [Get meeting recordings](/docs/api/rest/reference/zoom-api/methods/#operation/recordingGet) | `cloud_recording:read:list_recording_files:admin`, `cloud_recording:read:list_recording_files`, `cloud_recording:read:list_recording_files:master` | +| [Get meeting recordings](/docs/api/rest/reference/zoom-api/ma/#operation/recordingGet) | `cloud_recording:read:list_recording_files:master` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/recordingRegistrantQuestionUpdate) | `cloud_recording:update:registration_questions:admin`, `cloud_recording:update:registration_questions`, `cloud_recording:update:registration_questions:master` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/recordingRegistrantQuestionUpdate) | `cloud_recording:update:registration_questions:master` | +| [Delete a meeting or webinar transcript](/docs/api/rest/reference/zoom-api/methods/#operation/DeleteMeetingTranscript) | `cloud_recording:delete:meeting_transcript`, `cloud_recording:delete:meeting_transcript:admin` | + +### Devices + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Upgrade ZPA firmware or app](/docs/api/rest/reference/zoom-api/methods/#operation/UpgradeZpas/app) | `device:write:zpa_os_app:admin`, `device:write:zpa_os_app:master` | +| [Get ZPA version info](/docs/api/rest/reference/zoom-api/methods/#operation/GetZpaVersioninfo) | `device:read:list_zpa_versions:admin`, `device:read:list_zpa_versions:master` | +| [Assign a device to a user or commonarea](/docs/api/rest/reference/zoom-api/methods/#operation/Assigndevicetoauser/commonarea) | `device:write:zpa_device:admin`, `device:write:zpa_device:master` | +| [Delete ZPA device by vendor and mac address](/docs/api/rest/reference/zoom-api/methods/#operation/DeleteZpaDeviceByVendorAndMacAddress) | `device:delete:zpa_device:admin`, `device:delete:zpa_device:master` | +| [Get ZDM group info](/docs/api/rest/reference/zoom-api/methods/#operation/Getzdmgroupinfo) | `device:read:list_groups:admin`, `device:read:list_groups:master` | +| [Assign a device to a group](/docs/api/rest/reference/zoom-api/methods/#operation/assginGroup) | `device:write:group:admin`, `device:write:group:master` | +| [Get Zoom Phone Appliance settings by user ID](/docs/api/rest/reference/zoom-api/methods/#operation/GetZpaDeviceListProfileSettingOfaUser) | `device:read:user_setting:admin`, `device:read:user_setting:master` | +| [Change device association](/docs/api/rest/reference/zoom-api/methods/#operation/changeDeviceAssociation) | `device:update:zdm_device_assignment:admin` | + +### H323 Devices + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceCreate) | `h323_device:write:device:admin` | +| [Delete a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceDelete) | `h323_device:delete:device:admin` | +| [Update a H.323/SIP device](/docs/api/rest/reference/zoom-api/methods/#operation/deviceUpdate) | `h323_device:update:device:admin` | +| [List all H.323/SIP devices](/docs/api/rest/reference/zoom-api/methods/#operation/deviceList) | `h323_device:read:list_devices:admin` | + +### In-Meeting Apps + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppAdd) | `meeting:write:open_app`, `meeting:write:open_app:admin` | +| [Delete a meeting app](/docs/api/rest/reference/zoom-api/methods/#operation/meetingAppDelete) | `meeting:delete:open_app`, `meeting:delete:open_app:admin` | + +### In-Meeting Features + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a meeting's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLocalRecordingJoinToken) | `meeting:read:local_recording_token`, `meeting:read:local_recording_token:admin` | +| [Use in-meeting controls](/docs/api/rest/reference/zoom-api/methods/#operation/inMeetingControl) | `meeting:update:in_meeting_controls`, `meeting:update:in_meeting_controls:admin` | +| [Use in-meeting controls](/docs/api/rest/reference/zoom-api/ma/#operation/inMeetingControl) | `meeting:update:in_meeting_controls:master` | +| [Update a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/updateMeetingChatMessageById) | `meeting:update:live_meeting_chat_message`, `meeting:update:live_meeting_chat_message:admin` | +| [Get meeting's token](/docs/api/rest/reference/zoom-api/methods/#operation/meetingToken) | `meeting:read:token`, `meeting:read:token:admin` | +| [Get meeting's token](/docs/api/rest/reference/zoom-api/ma/#operation/meetingToken) | `meeting:read:token:master` | +| [Delete a live meeting message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteMeetingChatMessageById) | `meeting:delete:live_meeting_chat_message`, `meeting:delete:live_meeting_chat_message:admin` | + +### Invitation & Registration + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingregistrantdelete) | `meeting:delete:registrant`, `meeting:delete:registrant:admin` | +| [Delete a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingregistrantdelete) | `meeting:delete:registrant:master` | +| [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchRegistrants) | `meeting:write:batch_registrants`, `meeting:write:batch_registrants:admin` | +| [Perform batch registration](/docs/api/rest/reference/zoom-api/ma/#operation/addBatchRegistrants) | `meeting:write:batch_registrants:master` | +| [Get meeting invitation](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInvitation) | `meeting:read:invitation`, `meeting:read:invitation:admin` | +| [Get meeting invitation](/docs/api/rest/reference/zoom-api/ma/#operation/meetingInvitation) | `meeting:read:invitation:master` | +| [Add a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantCreate) | `meeting:write:registrant`, `meeting:write:registrant:admin` | +| [Add a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantCreate) | `meeting:write:registrant:master` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantQuestionUpdate) | `meeting:update:registration_question`, `meeting:update:registration_question:admin` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantQuestionUpdate) | `meeting:update:registration_question:master` | +| [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantStatus) | `meeting:update:registrant_status`, `meeting:update:registrant_status:admin` | +| [Update registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantStatus) | `meeting:update:registrant_status:master` | +| [Create a meeting's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/meetingInviteLinksCreate) | `meeting:write:invite_links`, `meeting:write:invite_links:admin` | +| [Create a meeting's invite links](/docs/api/rest/reference/zoom-api/ma/#operation/meetingInviteLinksCreate) | `meeting:write:invite_links:master` | +| [List meeting registrants](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrants) | `meeting:read:list_registrants`, `meeting:read:list_registrants:admin` | +| [List meeting registrants](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrants) | `meeting:read:list_registrants:master` | +| [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantsQuestionsGet) | `meeting:read:list_registration_questions`, `meeting:read:list_registration_questions:admin` | +| [Get a meeting registrant](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRegistrantGet) | `meeting:read:registrant`, `meeting:read:registrant:admin` | +| [Get a meeting registrant](/docs/api/rest/reference/zoom-api/ma/#operation/meetingRegistrantGet) | `meeting:read:registrant:master` | + +### Live streaming + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a livestream](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamUpdate) | `meeting:update:livestream`, `meeting:update:livestream:admin` | +| [Update a livestream](/docs/api/rest/reference/zoom-api/ma/#operation/meetingLiveStreamUpdate) | `meeting:update:livestream:master` | +| [Update livestream status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamStatusUpdate) | `meeting:update:livestream_status`, `meeting:update:livestream_status:admin` | +| [Update livestream status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingLiveStreamStatusUpdate) | `meeting:update:livestream_status:master` | +| [Get livestream details](/docs/api/rest/reference/zoom-api/methods/#operation/getMeetingLiveStreamDetails) | `meeting:read:livestream`, `meeting:read:livestream:admin` | +| [Get livestream details](/docs/api/rest/reference/zoom-api/ma/#operation/getMeetingLiveStreamDetails) | `meeting:read:livestream:master` | +| [Get a meeting's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/meetingLiveStreamingJoinToken) | `meeting:read:live_streaming_token`, `meeting:read:live_streaming_token:admin` | + +### Meetings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List past meeting instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetings) | `meeting:read:list_past_instances`, `meeting:read:list_past_instances:admin` | +| [List past meeting instances](/docs/api/rest/reference/zoom-api/ma/#operation/pastMeetings) | `meeting:read:list_past_instances:master` | +| [Get past meeting participants](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingParticipants) | `meeting:read:list_past_participants`, `meeting:read:list_past_participants:admin` | +| [Get a meeting SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getSipDialingWithPasscode) | `meeting:write:sip_dialing`, `meeting:write:sip_dialing:admin` | +| [Create a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate) | `meeting:write:meeting`, `meeting:write:meeting:admin` | +| [Create a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingCreate) | `meeting:write:meeting:master` | +| [Update meeting status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingStatus) | `meeting:update:status`, `meeting:update:status:admin` | +| [Update meeting status](/docs/api/rest/reference/zoom-api/ma/#operation/meetingStatus) | `meeting:update:status:master` | +| [Get past meeting details](/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingDetails) | `meeting:read:past_meeting`, `meeting:read:past_meeting:admin` | +| [List meetings](/docs/api/rest/reference/zoom-api/methods/#operation/meetings) | `meeting:read:list_meetings`, `meeting:read:list_meetings:admin` | +| [List meetings](/docs/api/rest/reference/zoom-api/ma/#operation/meetings) | `meeting:read:list_meetings:master` | +| [Update a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate) | `meeting:update:meeting:admin`, `meeting:update:meeting` | +| [Update a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingUpdate) | `meeting:update:meeting:master` | +| [Delete a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingDelete) | `meeting:delete:meeting`, `meeting:delete:meeting:admin` | +| [Delete a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingDelete) | `meeting:delete:meeting:master` | +| [Update participant Real-Time Media Streams (RTMS) app status](/docs/api/rest/reference/zoom-api/methods/#operation/meetingRTMSStatusUpdate) | `meeting:update:participant_rtms_app_status`, `meeting:update:participant_rtms_app_status:admin` | +| [Get a meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meeting) | `meeting:read:meeting`, `meeting:read:meeting:admin` | +| [Get a meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meeting) | `meeting:read:meeting:master` | +| [List upcoming meetings](/docs/api/rest/reference/zoom-api/methods/#operation/listUpcomingMeeting) | `meeting:read:list_upcoming_meetings`, `meeting:read:list_upcoming_meetings:admin` | +| [List past meetings' Q&A](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingQA) | `meeting:read:past_qa`, `meeting:read:past_qa:admin` | + +### PAC + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List a user's PAC accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userPACs) | `pac:read:list_pac_accounts`, `pac:read:list_pac_accounts:admin` | +| [List a user's PAC accounts](/docs/api/rest/reference/zoom-api/ma/#operation/userPACs) | `pac:read:list_pac_accounts:master` | + +### Polls + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollDelete) | `meeting:delete:poll`, `meeting:delete:poll:admin` | +| [Delete a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollDelete) | `meeting:delete:poll:master` | +| [List meeting polls](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPolls) | `meeting:read:list_polls`, `meeting:read:list_polls:admin` | +| [List meeting polls](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPolls) | `meeting:read:list_polls:master` | +| [List past meeting's poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastMeetingPolls) | `meeting:read:list_poll_results`, `meeting:read:list_poll_results:admin` | +| [Perform batch poll creation](/docs/api/rest/reference/zoom-api/methods/#operation/createBatchPolls) | `meeting:write:batch_polls`, `meeting:write:batch_polls:admin` | +| [Perform batch poll creation](/docs/api/rest/reference/zoom-api/ma/#operation/createBatchPolls) | `meeting:write:batch_polls:master` | +| [Create a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollCreate) | `meeting:write:poll`, `meeting:write:poll:admin` | +| [Create a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollCreate) | `meeting:write:poll:master` | +| [Get a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollGet) | `meeting:read:poll`, `meeting:read:poll:admin` | +| [Get a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollGet) | `meeting:read:poll:master` | +| [Update a meeting poll](/docs/api/rest/reference/zoom-api/methods/#operation/meetingPollUpdate) | `meeting:update:poll`, `meeting:update:poll:admin` | +| [Update a meeting poll](/docs/api/rest/reference/zoom-api/ma/#operation/meetingPollUpdate) | `meeting:update:poll:master` | + +### Reports + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a meeting activities report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingactivitylogs) | `report:read:meeting_activity_log:admin` | +| [Get a meeting activities report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingactivitylogs) | `report:read:meeting_activity_log:master` | +| [Get telephone reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportTelephone) | `report:read:telephone:admin` | +| [Get telephone reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportTelephone) | `report:read:telephone:master` | +| [Get meeting detail reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingDetails) | `report:read:meeting:admin` | +| [Get meeting detail reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingDetails) | `report:read:meeting:master` | +| [Get operation logs report](/docs/api/rest/reference/zoom-api/methods/#operation/reportOperationLogs) | `report:read:operation_logs:admin` | +| [Get operation logs report](/docs/api/rest/reference/zoom-api/ma/#operation/reportOperationLogs) | `report:read:operation_logs:master` | +| [Get meeting Q&A report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingQA) | `report:read:meeting_qna:admin` | +| [Get meeting Q&A report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingQA) | `report:read:meeting_qna:master` | +| [Get history meeting and webinar list](/docs/api/rest/reference/zoom-api/methods/#operation/Gethistorymeetingandwebinarlist) | `report:read:list_history_meetings:admin` | +| [Get history meeting and webinar list](/docs/api/rest/reference/zoom-api/ma/#operation/Gethistorymeetingandwebinarlist) | `report:read:list_history_meetings:master` | +| [Get remote support report](/docs/api/rest/reference/zoom-api/methods/#operation/Getremotesupportreport) | `report:read:remote_support:admin` | +| [Get remote support report](/docs/api/rest/reference/zoom-api/ma/#operation/Getremotesupportreport) | `report:read:remote_support:master` | +| [Get webinar poll reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarPolls) | `report:read:list_webinar_polls:admin` | +| [Get webinar poll reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarPolls) | `report:read:list_webinar_polls:master` | +| [Get webinar Q&A report](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarQA) | `report:read:webinar_qna:admin` | +| [Get webinar Q&A report](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarQA) | `report:read:webinar_qna:master` | +| [Get sign In / sign out activity report](/docs/api/rest/reference/zoom-api/methods/#operation/reportSignInSignOutActivities) | `report:read:user_activities:admin` | +| [Get sign In / sign out activity report](/docs/api/rest/reference/zoom-api/ma/#operation/reportSignInSignOutActivities) | `report:read:user_activities:master` | +| [Get meeting poll reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingPolls) | `report:read:list_meeting_polls:admin` | +| [Get meeting poll reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingPolls) | `report:read:list_meeting_polls:master` | +| [Get meeting reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetings) | `report:read:user:admin` | +| [Get meeting reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetings) | `report:read:user:master` | +| [Get active or inactive host reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportUsers) | `report:read:list_users:admin` | +| [Get active or inactive host reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportUsers) | `report:read:list_users:master` | +| [Get webinar participant reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarParticipants) | `report:read:list_webinar_participants:admin` | +| [Get webinar participant reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarParticipants) | `report:read:list_webinar_participants:master` | +| [Get upcoming events reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportUpcomingEvents) | `report:read:upcoming_meetings_webinars:admin` | +| [Get upcoming events reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportUpcomingEvents) | `report:read:upcoming_meetings_webinars:master` | +| [Get webinar survey report](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarSurvey) | `report:read:webinar_survey:admin` | +| [Get webinar survey report](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarSurvey) | `report:read:webinar_survey:master` | +| [Get meeting participant reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingParticipants) | `report:read:list_meeting_participants:admin` | +| [Get meeting participant reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingParticipants) | `report:read:list_meeting_participants:master` | +| [Get billing reports](/docs/api/rest/reference/zoom-api/methods/#operation/getBillingReport) | `report:read:billing:admin` | +| [Get billing reports](/docs/api/rest/reference/zoom-api/ma/#operation/getBillingReport) | `report:read:billing:master` | +| [Get webinar detail reports](/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarDetails) | `report:read:webinar:admin` | +| [Get webinar detail reports](/docs/api/rest/reference/zoom-api/ma/#operation/reportWebinarDetails) | `report:read:webinar:master` | +| [Get billing invoice reports](/docs/api/rest/reference/zoom-api/methods/#operation/getBillingInvoicesReports) | `report:read:billing_invoice:admin` | +| [Get billing invoice reports](/docs/api/rest/reference/zoom-api/ma/#operation/getBillingInvoicesReports) | `report:read:billing_invoice:master` | +| [Get meeting survey report](/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingSurvey) | `report:read:meeting_survey:admin` | +| [Get meeting survey report](/docs/api/rest/reference/zoom-api/ma/#operation/reportMeetingSurvey) | `report:read:meeting_survey:master` | + +### SIP Connected Audio + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List internal call-out countries](/docs/api/rest/reference/zoom-api/ma/#operation/ListInternalCall-outCountries) | `sip_connected_audio:read:callout_countries:master`, `sip_connected_audio:read:callout_countries:admin` | +| [List internal numbers](/docs/api/rest/reference/zoom-api/ma/#operation/ListInternalNumbers) | `sip_connected_audio:read:internal_numbers:master`, `sip_connected_audio:read:internal_numbers:admin` | +| [Add internal call-out countries](/docs/api/rest/reference/zoom-api/ma/#operation/AddInternalCall-outCountries) | `sip_connected_audio:write:callout_countries:admin`, `sip_connected_audio:write:callout_countries:master` | +| [Assign SIP trunk configuration](/docs/api/rest/reference/zoom-api/ma/#operation/AssignSIPTrunkConfiguration) | `sip_connected_audio:update:settings:master`, `sip_connected_audio:update:settings:admin` | +| [Add internal numbers](/docs/api/rest/reference/zoom-api/ma/#operation/AddInternalNumbers) | `sip_connected_audio:write:internal_numbers:admin`, `sip_connected_audio:write:internal_numbers:master` | +| [Get SIP trunk configuration](/docs/api/rest/reference/zoom-api/ma/#operation/GetSIPTrunkConfiguration) | `sip_connected_audio:read:settings:master`, `sip_connected_audio:read:settings:admin` | +| [List SIP trunks](/docs/api/rest/reference/zoom-api/ma/#operation/ListSIPTrunks) | `sip_connected_audio:read:trunks:master`, `sip_connected_audio:read:trunks:admin` | +| [Assign SIP trunks](/docs/api/rest/reference/zoom-api/ma/#operation/AssignSIPTrunks) | `sip_connected_audio:write:trunks:master` | +| [Delete a SIP trunk](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteASIPTrunk) | `sip_connected_audio:delete:trunks:master` | +| [Delete all numbers](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteAllNumbers) | `sip_connected_audio:delete:numbers:master` | +| [Assign numbers](/docs/api/rest/reference/zoom-api/ma/#operation/AssignNumbers) | `sip_connected_audio:write:numbers:master` | +| [Delete an internal number](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteAnInternalNumber) | `sip_connected_audio:delete:internal_numbers:admin`, `sip_connected_audio:delete:internal_numbers:master` | +| [List SIP trunk numbers](/docs/api/rest/reference/zoom-api/ma/#operation/listSipTrunkNumbers) | `sip_connected_audio:read:numbers:master` | +| [Update an internal number](/docs/api/rest/reference/zoom-api/ma/#operation/UpdateAnInternalNumber) | `sip_connected_audio:update:internal_numbers:master`, `sip_connected_audio:update:internal_numbers:admin` | +| [Delete internal call-out country](/docs/api/rest/reference/zoom-api/ma/#operation/DeleteInternalCall-outCountry) | `sip_connected_audio:delete:callout_countries:master`, `sip_connected_audio:delete:callout_countries:admin` | + +### SIP Phone + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Enable SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/EnableSIPPhonePhones) | `sip_phone:write:sip_phone:admin` | +| [Enable SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/EnableSIPPhonePhones) | `sip_phone:write:sip_phone:master` | +| [Update SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/UpdateSIPPhonePhones) | `sip_phone:update:sip_phone:admin` | +| [Update SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/UpdateSIPPhonePhones) | `sip_phone:update:sip_phone:master` | +| [List SIP phones](/docs/api/rest/reference/zoom-api/methods/#operation/ListSIPPhonePhones) | `sip_phone:read:list_sip_phones:admin` | +| [List SIP phones](/docs/api/rest/reference/zoom-api/ma/#operation/ListSIPPhonePhones) | `sip_phone:read:list_sip_phones:master` | +| [Delete SIP phone](/docs/api/rest/reference/zoom-api/methods/#operation/deleteSIPPhonePhones) | `sip_phone:delete:sip_phone:admin` | +| [Delete SIP phone](/docs/api/rest/reference/zoom-api/ma/#operation/deleteSIPPhonePhones) | `sip_phone:delete:sip_phone:master` | + +### Summaries + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List an account's meeting or webinar summaries](/docs/api/rest/reference/zoom-api/methods/#operation/Listmeetingsummaries) | `meeting:read:list_summaries:admin` | +| [List an account's meeting or webinar summaries](/docs/api/rest/reference/zoom-api/ma/#operation/Listmeetingsummaries) | `meeting:read:list_summaries:master` | +| [Delete a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Deletemeetingorwebinarsummary) | `meeting:delete:summary`, `meeting:delete:summary:admin` | +| [Delete a meeting or webinar summary](/docs/api/rest/reference/zoom-api/ma/#operation/Deletemeetingorwebinarsummary) | `meeting:delete:summary:master` | +| [Get a meeting or webinar summary](/docs/api/rest/reference/zoom-api/methods/#operation/Getameetingsummary) | `meeting:read:summary`, `meeting:read:summary:admin` | +| [Get a meeting or webinar summary](/docs/api/rest/reference/zoom-api/ma/#operation/Getameetingsummary) | `meeting:read:summary:master` | + +### Surveys + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyUpdate) | `meeting:update:survey`, `meeting:update:survey:admin` | +| [Update a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyUpdate) | `meeting:update:survey:master` | +| [Get a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyGet) | `meeting:read:survey`, `meeting:read:survey:admin` | +| [Get a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyGet) | `meeting:read:survey:master` | +| [Delete a meeting survey](/docs/api/rest/reference/zoom-api/methods/#operation/meetingSurveyDelete) | `meeting:delete:survey`, `meeting:delete:survey:admin` | +| [Delete a meeting survey](/docs/api/rest/reference/zoom-api/ma/#operation/meetingSurveyDelete) | `meeting:delete:survey:master` | + +### TSP + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPCreate) | `tsp:write:tsp_account`, `tsp:write:tsp_account:admin` | +| [Add a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPCreate) | `tsp:write:tsp_account:master` | +| [Get account's TSP information](/docs/api/rest/reference/zoom-api/methods/#operation/tsp) | `tsp:read:tsp:admin` | +| [Get account's TSP information](/docs/api/rest/reference/zoom-api/ma/#operation/tsp) | `tsp:read:tsp:master` | +| [Update a TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPUpdate) | `tsp:update:tsp_account`, `tsp:update:tsp_account:admin` | +| [Update a TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPUpdate) | `tsp:update:tsp_account:master` | +| [Delete a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPDelete) | `tsp:delete:tsp_account`, `tsp:delete:tsp_account:admin` | +| [Delete a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPDelete) | `tsp:delete:tsp_account:master` | +| [Set global dial-in URL for a TSP user](/docs/api/rest/reference/zoom-api/methods/#operation/tspUrlUpdate) | `tsp:update:tsp_settings`, `tsp:update:tsp_settings:admin` | +| [Set global dial-in URL for a TSP user](/docs/api/rest/reference/zoom-api/ma/#operation/tspUrlUpdate) | `tsp:update:tsp_settings:master` | +| [Get a user's TSP account](/docs/api/rest/reference/zoom-api/methods/#operation/userTSP) | `tsp:read:tsp_account`, `tsp:read:tsp_account:admin` | +| [Get a user's TSP account](/docs/api/rest/reference/zoom-api/ma/#operation/userTSP) | `tsp:read:tsp_account:master` | +| [Update an account's TSP information](/docs/api/rest/reference/zoom-api/methods/#operation/tspUpdate) | `tsp:update:tsp:admin` | +| [Update an account's TSP information](/docs/api/rest/reference/zoom-api/ma/#operation/tspUpdate) | `tsp:update:tsp:master` | +| [List user's TSP accounts](/docs/api/rest/reference/zoom-api/methods/#operation/userTSPs) | `tsp:read:list_tsp_accounts`, `tsp:read:list_tsp_accounts:admin` | +| [List user's TSP accounts](/docs/api/rest/reference/zoom-api/ma/#operation/userTSPs) | `tsp:read:list_tsp_accounts:master` | + +### Templates + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List meeting templates](/docs/api/rest/reference/zoom-api/methods/#operation/listMeetingTemplates) | `meeting:read:list_templates`, `meeting:read:list_templates:admin` | +| [List meeting templates](/docs/api/rest/reference/zoom-api/ma/#operation/listMeetingTemplates) | `meeting:read:list_templates:master` | +| [Create a meeting template from an existing meeting](/docs/api/rest/reference/zoom-api/methods/#operation/meetingTemplateCreate) | `meeting:write:template`, `meeting:write:template:admin` | +| [Create a meeting template from an existing meeting](/docs/api/rest/reference/zoom-api/ma/#operation/meetingTemplateCreate) | `meeting:write:template:master` | + +### Tracking Field + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldGet) | `tracking_field:read:tracking_field:admin` | +| [Get a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldGet) | `tracking_field:read:tracking_field:master` | +| [Delete a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldDelete) | `tracking_field:delete:tracking_field:admin` | +| [Delete a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldDelete) | `tracking_field:delete:tracking_field:master` | +| [List tracking fields](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldList) | `tracking_field:read:list_tracking_fields:admin` | +| [List tracking fields](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldList) | `tracking_field:read:list_tracking_fields:master` | +| [Create a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldCreate) | `tracking_field:write:tracking_field:admin` | +| [Create a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldCreate) | `tracking_field:write:tracking_field:master` | +| [Update a tracking field](/docs/api/rest/reference/zoom-api/methods/#operation/trackingfieldUpdate) | `tracking_field:update:tracking_field:admin` | +| [Update a tracking field](/docs/api/rest/reference/zoom-api/ma/#operation/trackingfieldUpdate) | `tracking_field:update:tracking_field:master` | + +### Webinars + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarDelete) | `webinar:delete:webinar`, `webinar:delete:webinar:admin` | +| [Delete a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarDelete) | `webinar:delete:webinar:master` | +| [List registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantsQuestionsGet) | `webinar:read:list_registration_questions`, `webinar:read:list_registration_questions:admin` | +| [List registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantsQuestionsGet) | `webinar:read:list_registration_questions:master` | +| [Get a webinar's join token for local recording](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLocalRecordingJoinToken) | `webinar:read:local_recording_token`, `webinar:read:local_recording_token:admin` | +| [Create a webinar's poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollCreate) | `webinar:write:poll`, `webinar:write:poll:admin` | +| [Create a webinar's poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollCreate) | `webinar:write:poll:master` | +| [Upload a webinar's branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingVB) | `webinar:write:branding_virtual_background`, `webinar:write:branding_virtual_background:admin` | +| [Upload a webinar's branding virtual background](/docs/api/rest/reference/zoom-api/ma/#operation/uploadWebinarBrandingVB) | `webinar:write:branding_virtual_background:master` | +| [List a webinar's polls](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPolls) | `webinar:read:list_polls`, `webinar:read:list_polls:admin` | +| [List a webinar's polls](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPolls) | `webinar:read:list_polls:master` | +| [Update registrant's status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantStatus) | `webinar:update:registrant_status`, `webinar:update:registrant_status:admin` | +| [Update registrant's status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantStatus) | `webinar:update:registrant_status:master` | +| [Update a live stream](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamUpdate) | `webinar:update:livestream`, `webinar:update:livestream:admin` | +| [Update a live stream](/docs/api/rest/reference/zoom-api/ma/#operation/webinarLiveStreamUpdate) | `webinar:update:livestream:master` | +| [Remove all panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistsDelete) | `webinar:delete:panelist`, `webinar:delete:panelist:admin` | +| [Remove all panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistsDelete) | `webinar:delete:panelist:master` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantQuestionUpdate) | `webinar:update:registration_question`, `webinar:update:registration_question:admin` | +| [Update registration questions](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantQuestionUpdate) | `webinar:update:registration_question:master` | +| [Delete a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyDelete) | `webinar:delete:survey`, `webinar:delete:survey:admin` | +| [Delete a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyDelete) | `webinar:delete:survey:master` | +| [Create a webinar template](/docs/api/rest/reference/zoom-api/methods/#operation/webinarTemplateCreate) | `webinar:write:template`, `webinar:write:template:admin` | +| [Create a webinar template](/docs/api/rest/reference/zoom-api/ma/#operation/webinarTemplateCreate) | `webinar:write:template:master` | +| [List webinar templates](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarTemplates) | `webinar:read:list_templates`, `webinar:read:list_templates:admin` | +| [List webinar templates](/docs/api/rest/reference/zoom-api/ma/#operation/listWebinarTemplates) | `webinar:read:list_templates:master` | +| [Get a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinar) | `webinar:read:webinar`, `webinar:read:webinar:admin` | +| [Get a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinar) | `webinar:read:webinar:master` | +| [Get live stream details](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarLiveStreamDetails) | `webinar:read:livestream`, `webinar:read:livestream:admin` | +| [Get live stream details](/docs/api/rest/reference/zoom-api/ma/#operation/getWebinarLiveStreamDetails) | `webinar:read:livestream:master` | +| [Upload a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/uploadWebinarBrandingWallpaper) | `webinar:write:branding_wallpaper`, `webinar:write:branding_wallpaper:admin` | +| [Upload a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/ma/#operation/uploadWebinarBrandingWallpaper) | `webinar:write:branding_wallpaper:master` | +| [Get a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollGet) | `webinar:read:poll`, `webinar:read:poll:admin` | +| [Get a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollGet) | `webinar:read:poll:master` | +| [Get a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyGet) | `webinar:read:survey`, `webinar:read:survey:admin` | +| [Get a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyGet) | `webinar:read:survey:master` | +| [Update a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollUpdate) | `webinar:update:poll`, `webinar:update:poll:admin` | +| [Update a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollUpdate) | `webinar:update:poll:master` | +| [List past webinar poll results](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarPollResults) | `webinar:read:list_past_polls`, `webinar:read:list_past_polls:admin` | +| [Get a webinar's archive token for local archiving](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLocalArchivingArchiveToken) | `webinar:read:local_archiving_token:admin` | +| [Create a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarCreate) | `webinar:write:webinar`, `webinar:write:webinar:admin` | +| [Create a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarCreate) | `webinar:write:webinar:master` | +| [List webinars](/docs/api/rest/reference/zoom-api/methods/#operation/webinars) | `webinar:read:list_webinars`, `webinar:read:list_webinars:admin` | +| [List webinars](/docs/api/rest/reference/zoom-api/ma/#operation/webinars) | `webinar:read:list_webinars:master` | +| [Set webinar's default branding virtual background](/docs/api/rest/reference/zoom-api/methods/#operation/setWebinarBrandingVB) | `webinar:update:branding_virtual_background`, `webinar:update:branding_virtual_background:admin` | +| [Set webinar's default branding virtual background](/docs/api/rest/reference/zoom-api/ma/#operation/setWebinarBrandingVB) | `webinar:update:branding_virtual_background:master` | +| [Get webinar's session branding](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarBranding) | `webinar:read:branding`, `webinar:read:branding:admin` | +| [Get webinar's session branding](/docs/api/rest/reference/zoom-api/ma/#operation/getWebinarBranding) | `webinar:read:branding:master` | +| [Get a webinar's join token for live streaming](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamingJoinToken) | `webinar:read:live_streaming_token`, `webinar:read:live_streaming_token:admin` | +| [Delete a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingWallpaper) | `webinar:delete:branding_wallpaper`, `webinar:delete:branding_wallpaper:admin` | +| [Delete a webinar's branding wallpaper](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingWallpaper) | `webinar:delete:branding_wallpaper:master` | +| [Get a webinar SIP URI with passcode](/docs/api/rest/reference/zoom-api/methods/#operation/getWebinarSipDialingWithPasscode) | `webinar:write:sip_dialing`, `webinar:write:sip_dialing:admin` | +| [Perform batch registration](/docs/api/rest/reference/zoom-api/methods/#operation/addBatchWebinarRegistrants) | `webinar:write:batch_registrants`, `webinar:write:batch_registrants:admin` | +| [Perform batch registration](/docs/api/rest/reference/zoom-api/ma/#operation/addBatchWebinarRegistrants) | `webinar:write:batch_registrants:master` | +| [Update a webinar](/docs/api/rest/reference/zoom-api/methods/#operation/webinarUpdate) | `webinar:update:webinar`, `webinar:update:webinar:admin` | +| [Update a webinar](/docs/api/rest/reference/zoom-api/ma/#operation/webinarUpdate) | `webinar:update:webinar:master` | +| [Update webinar status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarStatus) | `webinar:update:status`, `webinar:update:status:admin` | +| [Update webinar status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarStatus) | `webinar:update:status:master` | +| [Delete a webinar's branding virtual backgrounds](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingVB) | `webinar:delete:branding_virtual_background`, `webinar:delete:branding_virtual_background:admin` | +| [Delete a webinar's branding virtual backgrounds](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingVB) | `webinar:delete:branding_virtual_background:master` | +| [List panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelists) | `webinar:read:list_panelists`, `webinar:read:list_panelists:admin` | +| [List panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelists) | `webinar:read:list_panelists:master` | +| [Get webinar absentees](/docs/api/rest/reference/zoom-api/methods/#operation/webinarAbsentees) | `webinar:read:list_absentees`, `webinar:read:list_absentees:admin` | +| [Get webinar absentees](/docs/api/rest/reference/zoom-api/ma/#operation/webinarAbsentees) | `webinar:read:list_absentees:master` | +| [Add a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantCreate) | `webinar:write:registrant`, `webinar:write:registrant:admin` | +| [Add a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantCreate) | `webinar:write:registrant:master` | +| [Get webinar tracking sources](/docs/api/rest/reference/zoom-api/methods/#operation/getTrackingSources) | `webinar:read:list_tracking_sources`, `webinar:read:list_tracking_sources:admin` | +| [Get webinar tracking sources](/docs/api/rest/reference/zoom-api/ma/#operation/getTrackingSources) | `webinar:read:list_tracking_sources:master` | +| [Delete a webinar poll](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPollDelete) | `webinar:delete:poll`, `webinar:delete:poll:admin` | +| [Delete a webinar poll](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPollDelete) | `webinar:delete:poll:master` | +| [Get webinar's token](/docs/api/rest/reference/zoom-api/methods/#operation/webinarToken) | `webinar:read:token`, `webinar:read:token:admin` | +| [Get webinar's token](/docs/api/rest/reference/zoom-api/ma/#operation/webinarToken) | `webinar:read:token:master` | +| [Get a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantGet) | `webinar:read:registrant`, `webinar:read:registrant:admin` | +| [Get a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrantGet) | `webinar:read:registrant:master` | +| [Remove a panelist](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistDelete) | `webinar:delete:panelist`, `webinar:delete:panelist:admin` | +| [Remove a panelist](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistDelete) | `webinar:delete:panelist:master` | +| [Delete a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarBrandingNameTag) | `webinar:delete:branding_name_tag`, `webinar:delete:branding_name_tag:admin` | +| [Delete a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarBrandingNameTag) | `webinar:delete:branding_name_tag:master` | +| [Update a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/updateWebinarBrandingNameTag) | `webinar:update:branding_name_tag`, `webinar:update:branding_name_tag:admin` | +| [Update a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/updateWebinarBrandingNameTag) | `webinar:update:branding_name_tag:master` | +| [List Q&As of a past webinar](/docs/api/rest/reference/zoom-api/methods/#operation/listPastWebinarQA) | `webinar:read:past_qa`, `webinar:read:past_qa:admin` | +| [Add panelists](/docs/api/rest/reference/zoom-api/methods/#operation/webinarPanelistCreate) | `webinar:write:panelist`, `webinar:write:panelist:admin` | +| [Add panelists](/docs/api/rest/reference/zoom-api/ma/#operation/webinarPanelistCreate) | `webinar:write:panelist:master` | +| [List past webinar instances](/docs/api/rest/reference/zoom-api/methods/#operation/pastWebinars) | `webinar:read:list_past_instances`, `webinar:read:list_past_instances:admin` | +| [List past webinar instances](/docs/api/rest/reference/zoom-api/ma/#operation/pastWebinars) | `webinar:read:list_past_instances:master` | +| [Create webinar's invite links](/docs/api/rest/reference/zoom-api/methods/#operation/webinarInviteLinksCreate) | `webinar:write:invite_links`, `webinar:write:invite_links:admin` | +| [Create webinar's invite links](/docs/api/rest/reference/zoom-api/ma/#operation/webinarInviteLinksCreate) | `webinar:write:invite_links:master` | +| [Update live stream status](/docs/api/rest/reference/zoom-api/methods/#operation/webinarLiveStreamStatusUpdate) | `webinar:update:livestream_status`, `webinar:update:livestream_status:admin` | +| [Update live stream status](/docs/api/rest/reference/zoom-api/ma/#operation/webinarLiveStreamStatusUpdate) | `webinar:update:livestream_status:master` | +| [Update a webinar survey](/docs/api/rest/reference/zoom-api/methods/#operation/webinarSurveyUpdate) | `webinar:update:survey`, `webinar:update:survey:admin` | +| [Update a webinar survey](/docs/api/rest/reference/zoom-api/ma/#operation/webinarSurveyUpdate) | `webinar:update:survey:master` | +| [Delete a webinar registrant](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarRegistrant) | `webinar:delete:registrant`, `webinar:delete:registrant:admin` | +| [Delete a webinar registrant](/docs/api/rest/reference/zoom-api/ma/#operation/deleteWebinarRegistrant) | `webinar:delete:registrant:master` | +| [List webinar participants](/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarParticipants) | `webinar:read:list_past_participants:admin`, `webinar:read:list_past_participants` | +| [Create a webinar's branding name tag](/docs/api/rest/reference/zoom-api/methods/#operation/createWebinarBrandingNameTag) | `webinar:write:branding_name_tag`, `webinar:write:branding_name_tag:admin` | +| [Create a webinar's branding name tag](/docs/api/rest/reference/zoom-api/ma/#operation/createWebinarBrandingNameTag) | `webinar:write:branding_name_tag:master` | +| [List webinar registrants](/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrants) | `webinar:read:list_registrants`, `webinar:read:list_registrants:admin` | +| [List webinar registrants](/docs/api/rest/reference/zoom-api/ma/#operation/webinarRegistrants) | `webinar:read:list_registrants:master` | +| [Delete a live webinar message](/docs/api/rest/reference/zoom-api/methods/#operation/deleteWebinarChatMessageById) | `webinar:delete:live_webinar_chat_message`, `webinar:delete:live_webinar_chat_message:admin` | + +## Zoom Phone + + +### Accounts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/deleteOutboundCallerNumbers) | `phone:delete:customized_number:admin` | +| [Delete phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/deleteOutboundCallerNumbers) | `phone:delete:customized_number:master` | +| [Add phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/addOutboundCallerNumbers) | `phone:write:customized_number:admin` | +| [Add phone numbers for an account's customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/addOutboundCallerNumbers) | `phone:write:customized_number:master` | +| [List an account's customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/listCustomizeOutboundCallerNumbers) | `phone:read:list_customized_number:admin` | +| [List an account's customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/listCustomizeOutboundCallerNumbers) | `phone:read:list_customized_number:master` | +| [List an account's Zoom Phone settings](/docs/api/rest/reference/phone/methods/#operation/listZoomPhoneAccountSettings) | `phone:read:list_account_settings:admin` | +| [List an account's Zoom Phone settings](/docs/api/rest/reference/phone/ma/#operation/listZoomPhoneAccountSettings) | `phone:read:list_account_settings:master` | + +### Alerts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete an alert setting](/docs/api/rest/reference/phone/methods/#operation/DeleteAnAlertSetting) | `phone:delete:alert_setting:admin` | +| [Delete an alert setting](/docs/api/rest/reference/phone/ma/#operation/DeleteAnAlertSetting) | `phone:delete:alert_setting:master` | +| [Get alert setting details](/docs/api/rest/reference/phone/methods/#operation/GetAlertSettingDetails) | `phone:read:alert_setting:admin` | +| [Get alert setting details](/docs/api/rest/reference/phone/ma/#operation/GetAlertSettingDetails) | `phone:read:alert_setting:master` | +| [List alert settings with paging query](/docs/api/rest/reference/phone/methods/#operation/ListAlertSettingsWithPagingQuery) | `phone:read:list_alert_settings:admin` | +| [List alert settings with paging query](/docs/api/rest/reference/phone/ma/#operation/ListAlertSettingsWithPagingQuery) | `phone:read:list_alert_settings:master` | +| [Add an alert setting](/docs/api/rest/reference/phone/methods/#operation/AddAnAlertSetting) | `phone:write:alert_setting:admin` | +| [Add an alert setting](/docs/api/rest/reference/phone/ma/#operation/AddAnAlertSetting) | `phone:write:alert_setting:master` | +| [Update an alert setting](/docs/api/rest/reference/phone/methods/#operation/UpdateAnAlertSetting) | `phone:patch:alert_setting:admin` | +| [Update an alert setting](/docs/api/rest/reference/phone/ma/#operation/UpdateAnAlertSetting) | `phone:patch:alert_setting:master` | + +### Audio Library + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete an audio item](/docs/api/rest/reference/phone/methods/#operation/DeleteAudioItem) | `phone:delete:audio`, `phone:delete:audio:admin` | +| [Delete an audio item](/docs/api/rest/reference/phone/ma/#operation/DeleteAudioItem) | `phone:delete:audio:master` | +| [Add audio items](/docs/api/rest/reference/phone/methods/#operation/AddAudioItem) | `phone:write:batch_audios`, `phone:write:batch_audios:admin` | +| [Add audio items](/docs/api/rest/reference/phone/ma/#operation/AddAudioItem) | `phone:write:batch_audios:master` | +| [Update an audio item](/docs/api/rest/reference/phone/methods/#operation/UpdateAudioItem) | `phone:update:audio`, `phone:update:audio:admin` | +| [Update an audio item](/docs/api/rest/reference/phone/ma/#operation/UpdateAudioItem) | `phone:update:audio:master` | +| [Add an audio item for text-to-speech conversion](/docs/api/rest/reference/phone/methods/#operation/AddAnAudio) | `phone:write:audio`, `phone:write:audio:admin` | +| [Add an audio item for text-to-speech conversion](/docs/api/rest/reference/phone/ma/#operation/AddAnAudio) | `phone:write:audio:master` | +| [Get an audio item](/docs/api/rest/reference/phone/methods/#operation/GetAudioItem) | `phone:read:audio`, `phone:read:audio:admin` | +| [Get an audio item](/docs/api/rest/reference/phone/ma/#operation/GetAudioItem) | `phone:read:audio:master` | +| [List audio items](/docs/api/rest/reference/phone/methods/#operation/ListAudioItems) | `phone:read:list_audios`, `phone:read:list_audios:admin` | +| [List audio items](/docs/api/rest/reference/phone/ma/#operation/ListAudioItems) | `phone:read:list_audios:master` | + +### Auto Receptionists + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete a non-primary auto receptionist](/docs/api/rest/reference/phone/methods/#operation/deleteAutoReceptionist) | `phone:delete:auto_receptionist:admin` | +| [Delete a non-primary auto receptionist](/docs/api/rest/reference/phone/ma/#operation/deleteAutoReceptionist) | `phone:delete:auto_receptionist:master` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/methods/#operation/unassignAllPhoneNumsAutoReceptionist) | `phone:delete:auto_receptionist_number:admin` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/ma/#operation/unassignAllPhoneNumsAutoReceptionist) | `phone:delete:auto_receptionist_number:master` | +| [Get an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistDetail) | `phone:read:auto_receptionist:admin` | +| [Get an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistDetail) | `phone:read:auto_receptionist:master` | +| [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/unassignAPhoneNumAutoReceptionist) | `phone:delete:auto_receptionist_number:admin` | +| [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/unassignAPhoneNumAutoReceptionist) | `phone:delete:auto_receptionist_number:master` | +| [Update an auto receptionist policy](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionistPolicy) | `phone:update:auto_receptionist_policy:admin` | +| [Update an auto receptionist policy](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionistPolicy) | `phone:update:auto_receptionist_policy:master` | +| [Update an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionist) | `phone:update:auto_receptionist:admin` | +| [Update an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionist) | `phone:update:auto_receptionist:master` | +| [Add a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/AddPolicy) | `phone:write:auto_receptionist_policy:admin` | +| [Add a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/AddPolicy) | `phone:write:auto_receptionist_policy:master` | +| [List auto receptionists](/docs/api/rest/reference/phone/methods/#operation/listAutoReceptionists) | `phone:read:list_auto_receptionists:admin` | +| [List auto receptionists](/docs/api/rest/reference/phone/ma/#operation/listAutoReceptionists) | `phone:read:list_auto_receptionists:master` | +| [Add an auto receptionist](/docs/api/rest/reference/phone/methods/#operation/addAutoReceptionist) | `phone:write:auto_receptionist:admin` | +| [Add an auto receptionist](/docs/api/rest/reference/phone/ma/#operation/addAutoReceptionist) | `phone:write:auto_receptionist:master` | +| [Delete a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/DeletePolicy) | `phone:delete:auto_receptionist_policy:admin` | +| [Delete a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/DeletePolicy) | `phone:delete:auto_receptionist_policy:master` | +| [Assign phone numbers](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumbersAutoReceptionist) | `phone:write:auto_receptionist_number:admin` | +| [Assign phone numbers](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumbersAutoReceptionist) | `phone:write:auto_receptionist_number:master` | +| [Get an auto receptionist policy](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistsPolicy) | `phone:read:auto_receptionist_policy:admin` | +| [Get an auto receptionist policy](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistsPolicy) | `phone:read:auto_receptionist_policy:master` | +| [Update a policy subsetting](/docs/api/rest/reference/phone/methods/#operation/updatePolicy) | `phone:update:auto_receptionist_policy:admin` | +| [Update a policy subsetting](/docs/api/rest/reference/phone/ma/#operation/updatePolicy) | `phone:update:auto_receptionist_policy:master` | + +### Billing Account + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get billing account details](/docs/api/rest/reference/phone/methods/#operation/GetABillingAccount) | `phone:read:billing_account:admin` | +| [Get billing account details](/docs/api/rest/reference/phone/ma/#operation/GetABillingAccount) | `phone:read:billing_account:master` | +| [List billing accounts](/docs/api/rest/reference/phone/methods/#operation/listBillingAccount) | `phone:read:list_billing_accounts:admin` | +| [List billing accounts](/docs/api/rest/reference/phone/ma/#operation/listBillingAccount) | `phone:read:list_billing_accounts:master` | + +### Blocked List + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a blocked list](/docs/api/rest/reference/phone/methods/#operation/updateBlockedList) | `phone:update:blocked_list:admin` | +| [Update a blocked list](/docs/api/rest/reference/phone/ma/#operation/updateBlockedList) | `phone:update:blocked_list:master` | +| [List blocked lists](/docs/api/rest/reference/phone/methods/#operation/listBlockedList) | `phone:read:list_blocked_lists:admin` | +| [List blocked lists](/docs/api/rest/reference/phone/ma/#operation/listBlockedList) | `phone:read:list_blocked_lists:master` | +| [Create a blocked list](/docs/api/rest/reference/phone/methods/#operation/addAnumberToBlockedList) | `phone:write:blocked_list:admin` | +| [Create a blocked list](/docs/api/rest/reference/phone/ma/#operation/addAnumberToBlockedList) | `phone:write:blocked_list:master` | +| [Get blocked list details](/docs/api/rest/reference/phone/methods/#operation/getABlockedList) | `phone:read:blocked_list:admin` | +| [Get blocked list details](/docs/api/rest/reference/phone/ma/#operation/getABlockedList) | `phone:read:blocked_list:master` | +| [Delete a blocked list](/docs/api/rest/reference/phone/methods/#operation/deleteABlockedList) | `phone:delete:blocked_list:admin` | +| [Delete a blocked list](/docs/api/rest/reference/phone/ma/#operation/deleteABlockedList) | `phone:delete:blocked_list:master` | + +### Call Handling + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a call handling setting](/docs/api/rest/reference/phone/methods/#operation/updateCallHandling) | `phone:update:call_handling_setting:admin` | +| [Update a call handling setting](/docs/api/rest/reference/phone/ma/#operation/updateCallHandling) | `phone:update:call_handling_setting:master` | +| [Add a call handling setting](/docs/api/rest/reference/phone/methods/#operation/addCallHandling) | `phone:write:call_handling_setting:admin` | +| [Add a call handling setting](/docs/api/rest/reference/phone/ma/#operation/addCallHandling) | `phone:write:call_handling_setting:master` | +| [Delete a call handling setting](/docs/api/rest/reference/phone/methods/#operation/deleteCallHandling) | `phone:delete:call_handling_setting:admin` | +| [Delete a call handling setting](/docs/api/rest/reference/phone/ma/#operation/deleteCallHandling) | `phone:delete:call_handling_setting:master` | +| [Get call handling settings](/docs/api/rest/reference/phone/methods/#operation/getCallHandling) | `phone:read:list_call_handling_settings:admin` | +| [Get call handling settings](/docs/api/rest/reference/phone/ma/#operation/getCallHandling) | `phone:read:list_call_handling_settings:master` | + +### Call Logs + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get call log details](/docs/api/rest/reference/phone/methods/#operation/getCallLogDetails) | `phone:read:call_log:admin` | +| [Get call log details](/docs/api/rest/reference/phone/ma/#operation/getCallLogDetails) | `phone:read:call_log:master` | +| [Get account's call history](/docs/api/rest/reference/phone/methods/#operation/accountCallHistory) | `phone:read:list_call_logs:admin` | +| [Get account's call history](/docs/api/rest/reference/phone/ma/#operation/accountCallHistory) | `phone:read:list_call_logs:master` | +| [Get call history](/docs/api/rest/reference/phone/methods/#operation/getCallPath) | `phone:read:call_log:admin` | +| [Get call history](/docs/api/rest/reference/phone/ma/#operation/getCallPath) | `phone:read:call_log:master` | +| [Get call history detail](/docs/api/rest/reference/phone/methods/#operation/getCallHistoryDetail) | `phone:read:call_log:admin` | +| [Get call history detail](/docs/api/rest/reference/phone/ma/#operation/getCallHistoryDetail) | `phone:read:call_log:master` | +| [Delete a user's call log](/docs/api/rest/reference/phone/methods/#operation/deleteCallLog) | `phone:delete:call_log`, `phone:delete:call_log:admin` | +| [Delete a user's call log](/docs/api/rest/reference/phone/ma/#operation/deleteCallLog) | `phone:delete:call_log:master` | +| [Get User AI Call Summary Detail](/docs/api/rest/reference/phone/methods/#operation/getUserAICallSummary) | `phone:read:ai_call_summary`, `phone:read:ai_call_summary:admin` | +| [Get User AI Call Summary Detail](/docs/api/rest/reference/phone/ma/#operation/getUserAICallSummary) | `phone:read:ai_call_summary:master` | +| [Get user's call history](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallHistory) | `phone:read:list_call_logs:admin`, `phone:read:list_call_logs` | +| [Get user's call history](/docs/api/rest/reference/phone/ma/#operation/phoneUserCallHistory) | `phone:read:list_call_logs:master` | +| [Delete a user's call history](/docs/api/rest/reference/phone/methods/#operation/deleteUserCallHistory) | `phone:delete:call_log`, `phone:delete:call_log:admin` | +| [Delete a user's call history](/docs/api/rest/reference/phone/ma/#operation/deleteUserCallHistory) | `phone:delete:call_log:master` | +| [Get account's call logs](/docs/api/rest/reference/phone/methods/#operation/accountCallLogs) | `phone:read:list_call_logs:admin` | +| [Get account's call logs](/docs/api/rest/reference/phone/ma/#operation/accountCallLogs) | `phone:read:list_call_logs:master` | +| [Sync user's call history](/docs/api/rest/reference/phone/methods/#operation/syncUserCallHistory) | `phone:read:list_call_logs`, `phone:read:list_call_logs:admin` | +| [Sync user's call history](/docs/api/rest/reference/phone/ma/#operation/syncUserCallHistory) | `phone:read:list_call_logs:master` | +| [Add a client code to a call history](/docs/api/rest/reference/phone/methods/#operation/addClientCodeToCallHistory) | `phone:update:call_log:admin` | +| [Add a client code to a call history](/docs/api/rest/reference/phone/ma/#operation/addClientCodeToCallHistory) | `phone:update:call_log:master` | +| [Sync user's call logs](/docs/api/rest/reference/phone/methods/#operation/syncUserCallLogs) | `phone:read:list_call_logs`, `phone:read:list_call_logs:admin` | +| [Sync user's call logs](/docs/api/rest/reference/phone/ma/#operation/syncUserCallLogs) | `phone:read:list_call_logs:master` | +| [Get user's call logs](/docs/api/rest/reference/phone/methods/#operation/phoneUserCallLogs) | `phone:read:list_call_logs`, `phone:read:list_call_logs:admin` | +| [Get user's call logs](/docs/api/rest/reference/phone/ma/#operation/phoneUserCallLogs) | `phone:read:list_call_logs:master` | +| [Get call element](/docs/api/rest/reference/phone/methods/#operation/getCallElement) | `phone:read:call_log:admin` | +| [Get call element](/docs/api/rest/reference/phone/ma/#operation/getCallElement) | `phone:read:call_log:master` | +| [Add a client code to a call log](/docs/api/rest/reference/phone/methods/#operation/addClientCodeToCallLog) | `phone:update:call_log:admin` | +| [Add a client code to a call log](/docs/api/rest/reference/phone/ma/#operation/addClientCodeToCallLog) | `phone:update:call_log:master` | + +### Call Queues + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a call queue's policy subsetting](/docs/api/rest/reference/phone/methods/#operation/updateCQPolicySubSetting) | `phone:update:call_queue_policy:admin` | +| [Update a call queue's policy subsetting](/docs/api/rest/reference/phone/ma/#operation/updateCQPolicySubSetting) | `phone:update:call_queue_policy:master` | +| [Get call queue recordings](/docs/api/rest/reference/phone/methods/#operation/getCallQueueRecordings) | `phone:read:list_call_queue_recordings:admin` | +| [Get call queue recordings](/docs/api/rest/reference/phone/ma/#operation/getCallQueueRecordings) | `phone:read:list_call_queue_recordings:master` | +| [Assign numbers to a call queue](/docs/api/rest/reference/phone/methods/#operation/assignPhoneToCallQueue) | `phone:write:call_queue_number:admin` | +| [Assign numbers to a call queue](/docs/api/rest/reference/phone/ma/#operation/assignPhoneToCallQueue) | `phone:write:call_queue_number:master` | +| [List call queue members](/docs/api/rest/reference/phone/methods/#operation/listCallQueueMembers) | `phone:read:list_call_queue_members:admin` | +| [List call queue members](/docs/api/rest/reference/phone/ma/#operation/listCallQueueMembers) | `phone:read:list_call_queue_members:master` | +| [Unassign a member](/docs/api/rest/reference/phone/methods/#operation/unassignMemberFromCallQueue) | `phone:delete:call_queue_member:admin` | +| [Unassign a member](/docs/api/rest/reference/phone/ma/#operation/unassignMemberFromCallQueue) | `phone:delete:call_queue_member:master` | +| [List call queues](/docs/api/rest/reference/phone/methods/#operation/listCallQueues) | `phone:read:list_call_queues:admin` | +| [List call queues](/docs/api/rest/reference/phone/ma/#operation/listCallQueues) | `phone:read:list_call_queues:master` | +| [List call queue analytics](/docs/api/rest/reference/phone/methods/#operation/callqueueanalytics) | `phone:read:list_call_queues:admin` | +| [List call queue analytics](/docs/api/rest/reference/phone/ma/#operation/callqueueanalytics) | `phone:read:list_call_queues:master` | +| [Create a call queue](/docs/api/rest/reference/phone/methods/#operation/createCallQueue) | `phone:write:call_queue:admin` | +| [Create a call queue](/docs/api/rest/reference/phone/ma/#operation/createCallQueue) | `phone:write:call_queue:master` | +| [Unassign all members](/docs/api/rest/reference/phone/methods/#operation/unassignAllMembers) | `phone:delete:call_queue_member:admin` | +| [Unassign all members](/docs/api/rest/reference/phone/ma/#operation/unassignAllMembers) | `phone:delete:call_queue_member:master` | +| [Add a policy subsetting to a call queue](/docs/api/rest/reference/phone/methods/#operation/addCQPolicySubSetting) | `phone:write:call_queue_policy:admin` | +| [Add a policy subsetting to a call queue](/docs/api/rest/reference/phone/ma/#operation/addCQPolicySubSetting) | `phone:write:call_queue_policy:master` | +| [Add members to a call queue](/docs/api/rest/reference/phone/methods/#operation/addMembersToCallQueue) | `phone:write:call_queue_member:admin` | +| [Add members to a call queue](/docs/api/rest/reference/phone/ma/#operation/addMembersToCallQueue) | `phone:write:call_queue_member:master` | +| [Delete a call queue](/docs/api/rest/reference/phone/methods/#operation/deleteACallQueue) | `phone:delete:call_queue:admin` | +| [Delete a call queue](/docs/api/rest/reference/phone/ma/#operation/deleteACallQueue) | `phone:delete:call_queue:master` | +| [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/unAssignPhoneNumCallQueue) | `phone:delete:call_queue_number:admin` | +| [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/unAssignPhoneNumCallQueue) | `phone:delete:call_queue_number:master` | +| [Get call queue details](/docs/api/rest/reference/phone/methods/#operation/getACallQueue) | `phone:read:call_queue:admin` | +| [Get call queue details](/docs/api/rest/reference/phone/ma/#operation/getACallQueue) | `phone:read:call_queue:master` | +| [Update call queue details](/docs/api/rest/reference/phone/methods/#operation/updateCallQueue) | `phone:update:call_queue:admin` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/methods/#operation/unassignAPhoneNumCallQueue) | `phone:delete:call_queue_number:admin` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/ma/#operation/unassignAPhoneNumCallQueue) | `phone:delete:call_queue_number:master` | +| [Delete a CQ policy setting](/docs/api/rest/reference/phone/methods/#operation/removeCQPolicySubSetting) | `phone:delete:call_queue_policy:admin` | +| [Delete a CQ policy setting](/docs/api/rest/reference/phone/ma/#operation/removeCQPolicySubSetting) | `phone:delete:call_queue_policy:master` | + +### Carrier Reseller + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List phone numbers](/docs/api/rest/reference/phone/methods/#operation/listCRPhoneNumbers) | `phone:read:list_carrier_numbers:admin` | +| [List phone numbers](/docs/api/rest/reference/phone/ma/#operation/listCRPhoneNumbers) | `phone:read:list_carrier_numbers:master` | +| [Delete a phone number](/docs/api/rest/reference/phone/methods/#operation/deleteCRPhoneNumber) | `phone:delete:carrier_number:admin` | +| [Delete a phone number](/docs/api/rest/reference/phone/ma/#operation/deleteCRPhoneNumber) | `phone:delete:carrier_number:master` | +| [Create phone numbers](/docs/api/rest/reference/phone/methods/#operation/createCRPhoneNumbers) | `phone:write:carrier_number:admin` | +| [Create phone numbers](/docs/api/rest/reference/phone/ma/#operation/createCRPhoneNumbers) | `phone:write:carrier_number:master` | +| [Activate phone numbers](/docs/api/rest/reference/phone/methods/#operation/activeCRPhoneNumbers) | `phone:update:carrier_number:admin` | +| [Activate phone numbers](/docs/api/rest/reference/phone/ma/#operation/activeCRPhoneNumbers) | `phone:update:carrier_number:master` | + +### Common Areas + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Assign phone numbers to a common area](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumbersToCommonArea) | `phone:write:common_area_number:admin` | +| [Assign phone numbers to a common area](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumbersToCommonArea) | `phone:write:common_area_number:master` | +| [Add a common area](/docs/api/rest/reference/phone/methods/#operation/addCommonArea) | `phone:write:common_area:admin` | +| [Add a common area](/docs/api/rest/reference/phone/ma/#operation/addCommonArea) | `phone:write:common_area:master` | +| [Assign calling plans to a common area](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlansToCommonArea) | `phone:write:common_area_calling_plan:admin` | +| [Assign calling plans to a common area](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlansToCommonArea) | `phone:write:common_area_calling_plan:master` | +| [List common areas](/docs/api/rest/reference/phone/methods/#operation/listCommonAreas) | `phone:read:common_area:admin` | +| [List common areas](/docs/api/rest/reference/phone/ma/#operation/listCommonAreas) | `phone:read:common_area:master` | +| [Delete common area setting](/docs/api/rest/reference/phone/methods/#operation/deleteCommonAreaSetting) | `phone:delete:common_area_setting:admin` | +| [Delete common area setting](/docs/api/rest/reference/phone/ma/#operation/deleteCommonAreaSetting) | `phone:delete:common_area_setting:master` | +| [Apply template to common areas](/docs/api/rest/reference/phone/methods/#operation/ApplyTemplatetoCommonAreas) | `phone:write:apply_template_to_common_areas:admin` | +| [Apply template to common areas](/docs/api/rest/reference/phone/ma/#operation/ApplyTemplatetoCommonAreas) | `phone:write:apply_template_to_common_areas:master` | +| [Delete a common area](/docs/api/rest/reference/phone/methods/#operation/deleteCommonArea) | `phone:delete:common_area:admin` | +| [Delete a common area](/docs/api/rest/reference/phone/ma/#operation/deleteCommonArea) | `phone:delete:common_area:master` | +| [Get common area details](/docs/api/rest/reference/phone/methods/#operation/getACommonArea) | `phone:read:common_area:admin` | +| [Get common area details](/docs/api/rest/reference/phone/ma/#operation/getACommonArea) | `phone:read:common_area:master` | +| [Get common area settings](/docs/api/rest/reference/phone/methods/#operation/getCommonAreaSettings) | `phone:read:list_common_area_settings:admin` | +| [Get common area settings](/docs/api/rest/reference/phone/ma/#operation/getCommonAreaSettings) | `phone:read:list_common_area_settings:master` | +| [Unassign a calling plan from the common area](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlansFromCommonArea) | `phone:delete:common_area_calling_plan:admin` | +| [Unassign a calling plan from the common area](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlansFromCommonArea) | `phone:delete:common_area_calling_plan:master` | +| [Add common area setting](/docs/api/rest/reference/phone/methods/#operation/AddCommonAreaSetting) | `phone:write:common_area_setting:admin` | +| [Add common area setting](/docs/api/rest/reference/phone/ma/#operation/AddCommonAreaSetting) | `phone:write:common_area_setting:master` | +| [List activation codes](/docs/api/rest/reference/phone/methods/#operation/listActivationCodes) | `phone:read:list_common_area_activation_codes:admin` | +| [List activation codes](/docs/api/rest/reference/phone/ma/#operation/listActivationCodes) | `phone:read:list_common_area_activation_codes:master` | +| [Generate activation codes for common areas](/docs/api/rest/reference/phone/methods/#operation/Generateactivationcodesforcommonareas) | `phone:write:common_area:admin` | +| [Generate activation codes for common areas](/docs/api/rest/reference/phone/ma/#operation/Generateactivationcodesforcommonareas) | `phone:write:common_area:master` | +| [Unassign phone numbers from common area](/docs/api/rest/reference/phone/methods/#operation/unassignPhoneNumbersFromCommonArea) | `phone:delete:common_area_number:admin` | +| [Unassign phone numbers from common area](/docs/api/rest/reference/phone/ma/#operation/unassignPhoneNumbersFromCommonArea) | `phone:delete:common_area_number:master` | +| [Update common area](/docs/api/rest/reference/phone/methods/#operation/updateCommonArea) | `phone:update:common_area:admin` | +| [Update common area](/docs/api/rest/reference/phone/ma/#operation/updateCommonArea) | `phone:update:common_area:master` | +| [Update common area pin code](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaPinCode) | `phone:update:common_area:admin` | +| [Update common area pin code](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaPinCode) | `phone:update:common_area:master` | +| [Update common area setting](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaSetting) | `phone:update:common_area_setting:admin` | +| [Update common area setting](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaSetting) | `phone:update:common_area_setting:master` | + +### Dashboard + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List detectable personal location users](/docs/api/rest/reference/phone/methods/#operation/listUserDetectablePersonalLocation) | `phone:read:detectable_personal_location:admin` | +| [List detectable personal location users](/docs/api/rest/reference/phone/ma/#operation/listUserDetectablePersonalLocation) | `phone:read:detectable_personal_location:master` | +| [List real time location for users](/docs/api/rest/reference/phone/methods/#operation/listUserRealtimeLocation) | `phone:read:realtime_location_users:admin` | +| [List real time location for users](/docs/api/rest/reference/phone/ma/#operation/listUserRealtimeLocation) | `phone:read:realtime_location_users:master` | +| [List real time location for IP phones](/docs/api/rest/reference/phone/methods/#operation/listPhoneRealtimelocation) | `phone:read:realtime_location_devices:admin` | +| [List real time location for IP phones](/docs/api/rest/reference/phone/ma/#operation/listPhoneRealtimelocation) | `phone:read:realtime_location_devices:master` | +| [List default emergency address users](/docs/api/rest/reference/phone/methods/#operation/listUserDefaultEmergencyAddress) | `phone:read:default_emergency_address:admin` | +| [List default emergency address users](/docs/api/rest/reference/phone/ma/#operation/listUserDefaultEmergencyAddress) | `phone:read:default_emergency_address:master` | +| [List users permission for location sharing](/docs/api/rest/reference/phone/methods/#operation/listUserLocationSharingPermission) | `phone:read:location_sharing_permission:admin` | +| [List users permission for location sharing](/docs/api/rest/reference/phone/ma/#operation/listUserLocationSharingPermission) | `phone:read:location_sharing_permission:master` | +| [List nomadic emergency services users](/docs/api/rest/reference/phone/methods/#operation/listUserNomadicEmergencyServices) | `phone:read:nomadic_emergency_services:admin` | +| [List nomadic emergency services users](/docs/api/rest/reference/phone/ma/#operation/listUserNomadicEmergencyServices) | `phone:read:nomadic_emergency_services:master` | +| [Get call QoS](/docs/api/rest/reference/phone/methods/#operation/getCallQoS) | `phone:read:call_qos:admin` | +| [Get call QoS](/docs/api/rest/reference/phone/ma/#operation/getCallQoS) | `phone:read:call_qos:master` | +| [Get call details from call log](/docs/api/rest/reference/phone/methods/#operation/getCallLogMetricsDetails) | `phone:read:call_log:admin` | +| [Get call details from call log](/docs/api/rest/reference/phone/ma/#operation/getCallLogMetricsDetails) | `phone:read:call_log:master` | +| [List past call metrics](/docs/api/rest/reference/phone/methods/#operation/listPastCallMetrics) | `phone:read:list_call_logs:admin` | +| [List past call metrics](/docs/api/rest/reference/phone/ma/#operation/listPastCallMetrics) | `phone:read:list_call_logs:master` | +| [List call logs](/docs/api/rest/reference/phone/methods/#operation/listCallLogsMetrics) | `phone:read:list_call_logs:admin` | +| [List call logs](/docs/api/rest/reference/phone/ma/#operation/listCallLogsMetrics) | `phone:read:list_call_logs:master` | +| [List tracked locations](/docs/api/rest/reference/phone/methods/#operation/listTrackedLocations) | `phone:read:list_tracked_locations:admin` | +| [List tracked locations](/docs/api/rest/reference/phone/ma/#operation/listTrackedLocations) | `phone:read:list_tracked_locations:master` | + +### Device Line Keys + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get device line keys information](/docs/api/rest/reference/phone/methods/#operation/listDeviceLineKeySetting) | `phone:read:device_line_keys`, `phone:read:device_line_keys:admin` | +| [Get device line keys information](/docs/api/rest/reference/phone/ma/#operation/listDeviceLineKeySetting) | `phone:read:device_line_keys:master` | +| [Batch update device line key position](/docs/api/rest/reference/phone/methods/#operation/batchUpdateDeviceLineKeySetting) | `phone:update:device_line_keys`, `phone:update:device_line_keys:admin` | +| [Batch update device line key position](/docs/api/rest/reference/phone/ma/#operation/batchUpdateDeviceLineKeySetting) | `phone:update:device_line_keys:master` | + +### Dial by Name Directory + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete users from a directory](/docs/api/rest/reference/phone/methods/#operation/DeleteUsersFromDirectory) | `phone:delete:directory:admin` | +| [Delete users from a directory](/docs/api/rest/reference/phone/ma/#operation/DeleteUsersFromDirectory) | `phone:delete:directory:master` | +| [Delete users from a directory of a site](/docs/api/rest/reference/phone/methods/#operation/DeleteUsersFromDirectoryBySite) | `phone:delete:directory:admin` | +| [Delete users from a directory of a site](/docs/api/rest/reference/phone/ma/#operation/DeleteUsersFromDirectoryBySite) | `phone:delete:directory:master` | +| [List users in directory](/docs/api/rest/reference/phone/methods/#operation/ListUsersFromDirectory) | `phone:read:directory:admin` | +| [List users in directory](/docs/api/rest/reference/phone/ma/#operation/ListUsersFromDirectory) | `phone:read:directory:master` | +| [List users in a directory by site](/docs/api/rest/reference/phone/methods/#operation/ListUsersFromDirectoryBySite) | `phone:read:directory:admin` | +| [List users in a directory by site](/docs/api/rest/reference/phone/ma/#operation/ListUsersFromDirectoryBySite) | `phone:read:directory:master` | +| [Add users to a directory](/docs/api/rest/reference/phone/methods/#operation/AddUsersToDirectory) | `phone:write:directory:admin` | +| [Add users to a directory](/docs/api/rest/reference/phone/ma/#operation/AddUsersToDirectory) | `phone:write:directory:master` | +| [Add users to a directory of a site](/docs/api/rest/reference/phone/methods/#operation/AddUsersToDirectoryBySite) | `phone:write:directory:admin` | +| [Add users to a directory of a site](/docs/api/rest/reference/phone/ma/#operation/AddUsersToDirectoryBySite) | `phone:write:directory:master` | + +### Emergency Addresses + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update an emergency address](/docs/api/rest/reference/phone/methods/#operation/updateEmergencyAddress) | `phone:update:emergency_address:admin` | +| [Update an emergency address](/docs/api/rest/reference/phone/ma/#operation/updateEmergencyAddress) | `phone:update:emergency_address:master` | +| [Delete an emergency address](/docs/api/rest/reference/phone/methods/#operation/deleteEmergencyAddress) | `phone:delete:emergency_address:admin` | +| [Delete an emergency address](/docs/api/rest/reference/phone/ma/#operation/deleteEmergencyAddress) | `phone:delete:emergency_address:master` | +| [List emergency addresses](/docs/api/rest/reference/phone/methods/#operation/listEmergencyAddresses) | `phone:read:list_emergency_addresses:admin` | +| [List emergency addresses](/docs/api/rest/reference/phone/ma/#operation/listEmergencyAddresses) | `phone:read:list_emergency_addresses:master` | +| [Add an emergency address](/docs/api/rest/reference/phone/methods/#operation/addEmergencyAddress) | `phone:write:emergency_address:admin` | +| [Add an emergency address](/docs/api/rest/reference/phone/ma/#operation/addEmergencyAddress) | `phone:write:emergency_address:master` | +| [Get emergency address details](/docs/api/rest/reference/phone/methods/#operation/getEmergencyAddress) | `phone:read:emergency_address:admin` | +| [Get emergency address details](/docs/api/rest/reference/phone/ma/#operation/getEmergencyAddress) | `phone:read:emergency_address:master` | + +### Emergency Service Locations + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List emergency service locations](/docs/api/rest/reference/phone/methods/#operation/listLocations) | `phone:read:list_emergency_locations:admin` | +| [List emergency service locations](/docs/api/rest/reference/phone/ma/#operation/listLocations) | `phone:read:list_emergency_locations:master` | +| [Delete an emergency location](/docs/api/rest/reference/phone/methods/#operation/deleteLocation) | `phone:delete:emergency_location:admin` | +| [Delete an emergency location](/docs/api/rest/reference/phone/ma/#operation/deleteLocation) | `phone:delete:emergency_location:master` | +| [Batch add emergency service locations](/docs/api/rest/reference/phone/methods/#operation/batchAddLocations) | `phone:write:batch_emergency_locations:admin` | +| [Batch add emergency service locations](/docs/api/rest/reference/phone/ma/#operation/batchAddLocations) | `phone:write:batch_emergency_locations:master` | +| [Update emergency service location](/docs/api/rest/reference/phone/methods/#operation/updateLocation) | `phone:update:emergency_location:admin` | +| [Update emergency service location](/docs/api/rest/reference/phone/ma/#operation/updateLocation) | `phone:update:emergency_location:master` | +| [Add an emergency service location](/docs/api/rest/reference/phone/methods/#operation/addLocation) | `phone:write:emergency_location:admin` | +| [Add an emergency service location](/docs/api/rest/reference/phone/ma/#operation/addLocation) | `phone:write:emergency_location:master` | +| [Get emergency service location details](/docs/api/rest/reference/phone/methods/#operation/getLocation) | `phone:read:emergency_location:admin` | +| [Get emergency service location details](/docs/api/rest/reference/phone/ma/#operation/getLocation) | `phone:read:emergency_location:master` | + +### External Contacts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List external contacts](/docs/api/rest/reference/phone/methods/#operation/listExternalContacts) | `phone:read:list_external_contacts:admin` | +| [List external contacts](/docs/api/rest/reference/phone/ma/#operation/listExternalContacts) | `phone:read:list_external_contacts:master` | +| [Add an external contact](/docs/api/rest/reference/phone/methods/#operation/addExternalContact) | `phone:write:external_contact:admin` | +| [Add an external contact](/docs/api/rest/reference/phone/ma/#operation/addExternalContact) | `phone:write:external_contact:master` | +| [Update external contact](/docs/api/rest/reference/phone/methods/#operation/updateExternalContact) | `phone:update:external_contact:admin` | +| [Update external contact](/docs/api/rest/reference/phone/ma/#operation/updateExternalContact) | `phone:update:external_contact:master` | +| [Delete an external contact](/docs/api/rest/reference/phone/methods/#operation/deleteAExternalContact) | `phone:delete:external_contact:admin` | +| [Delete an external contact](/docs/api/rest/reference/phone/ma/#operation/deleteAExternalContact) | `phone:delete:external_contact:master` | +| [Get external contact details](/docs/api/rest/reference/phone/methods/#operation/getAExternalContact) | `phone:read:external_contact:admin` | +| [Get external contact details](/docs/api/rest/reference/phone/ma/#operation/getAExternalContact) | `phone:read:external_contact:master` | + +### Fax + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get extension's fax logs](/docs/api/rest/reference/phone/methods/#operation/Getuser'sfaxlogs) | `phone:read:list_fax_log`, `phone:read:list_fax_log:admin` | +| [Get extension's fax logs](/docs/api/rest/reference/phone/ma/#operation/Getuser'sfaxlogs) | `phone:read:list_fax_log:master` | +| [Download fax file](/docs/api/rest/reference/phone/methods/#operation/Downloadfaxfile) | `phone:read:fax_log`, `phone:read:fax_log:admin` | +| [Download fax file](/docs/api/rest/reference/phone/ma/#operation/Downloadfaxfile) | `phone:read:fax_log:master` | +| [Get account's fax logs](/docs/api/rest/reference/phone/methods/#operation/GetAccount'sFaxLogs) | `phone:read:list_fax_log:admin` | +| [Get account's fax logs](/docs/api/rest/reference/phone/ma/#operation/GetAccount'sFaxLogs) | `phone:read:list_fax_log:master` | +| [Get fax log details](/docs/api/rest/reference/phone/methods/#operation/GetFaxLogDetails) | `phone:read:list_fax_log:admin` | +| [Get fax log details](/docs/api/rest/reference/phone/ma/#operation/GetFaxLogDetails) | `phone:read:list_fax_log:master` | +| [Send fax](/docs/api/rest/reference/phone/methods/#operation/SendEFax) | `phone:write:send_fax`, `phone:write:send_fax:admin` | +| [Send fax](/docs/api/rest/reference/phone/ma/#operation/SendEFax) | `phone:write:send_fax:master` | +| [Upload fax file](/docs/api/rest/reference/phone/methods/#operation/UploadFaxFiles) | `phone:write:send_fax`, `phone:write:send_fax:admin` | +| [Upload fax file](/docs/api/rest/reference/phone/ma/#operation/UploadFaxFiles) | `phone:write:send_fax:master` | + +### Firmware Update Rules + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get firmware update rule information](/docs/api/rest/reference/phone/methods/#operation/GetFirmwareRuleDetail) | `phone:read:firmware_update_rule:admin` | +| [Get firmware update rule information](/docs/api/rest/reference/phone/ma/#operation/GetFirmwareRuleDetail) | `phone:read:firmware_update_rule:master` | +| [Delete firmware update rule](/docs/api/rest/reference/phone/methods/#operation/DeleteFirmwareUpdateRule) | `phone:delete:firmware_update_rule:admin` | +| [Delete firmware update rule](/docs/api/rest/reference/phone/ma/#operation/DeleteFirmwareUpdateRule) | `phone:delete:firmware_update_rule:master` | +| [List firmware update rules](/docs/api/rest/reference/phone/methods/#operation/ListFirmwareRules) | `phone:read:list_firmware_update_rules:admin` | +| [List firmware update rules](/docs/api/rest/reference/phone/ma/#operation/ListFirmwareRules) | `phone:read:list_firmware_update_rules:master` | +| [Update firmware update rule](/docs/api/rest/reference/phone/methods/#operation/UpdateFirmwareRule) | `phone:update:firmware_update_rule:admin` | +| [Update firmware update rule](/docs/api/rest/reference/phone/ma/#operation/UpdateFirmwareRule) | `phone:update:firmware_update_rule:master` | +| [Add a firmware update rule](/docs/api/rest/reference/phone/methods/#operation/AddFirmwareRule) | `phone:write:firmware_update_rule:admin` | +| [Add a firmware update rule](/docs/api/rest/reference/phone/ma/#operation/AddFirmwareRule) | `phone:write:firmware_update_rule:master` | +| [List updatable firmwares](/docs/api/rest/reference/phone/methods/#operation/ListFirmwares) | `phone:read:list_firmwares:admin` | +| [List updatable firmwares](/docs/api/rest/reference/phone/ma/#operation/ListFirmwares) | `phone:read:list_firmwares:master` | + +### Group Call Pickup + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update the group call pickup information](/docs/api/rest/reference/phone/methods/#operation/updateGCP) | `phone:update:call_pickup_group:admin` | +| [Update the group call pickup information](/docs/api/rest/reference/phone/ma/#operation/updateGCP) | `phone:update:call_pickup_group:master` | +| [List call pickup group members](/docs/api/rest/reference/phone/methods/#operation/listGCPMembers) | `phone:read:call_pickup_group_member:admin` | +| [List call pickup group members](/docs/api/rest/reference/phone/ma/#operation/listGCPMembers) | `phone:read:call_pickup_group_member:master` | +| [List group call pickup objects](/docs/api/rest/reference/phone/methods/#operation/listGCP) | `phone:read:list_call_pickup_groups:admin` | +| [List group call pickup objects](/docs/api/rest/reference/phone/ma/#operation/listGCP) | `phone:read:list_call_pickup_groups:master` | +| [Delete group call pickup objects](/docs/api/rest/reference/phone/methods/#operation/deleteGCP) | `phone:delete:call_pickup_group:admin` | +| [Delete group call pickup objects](/docs/api/rest/reference/phone/ma/#operation/deleteGCP) | `phone:delete:call_pickup_group:master` | +| [Add a group call pickup object](/docs/api/rest/reference/phone/methods/#operation/addGCP) | `phone:write:call_pickup_group:admin` | +| [Add a group call pickup object](/docs/api/rest/reference/phone/ma/#operation/addGCP) | `phone:write:call_pickup_group:master` | +| [Get call pickup group by ID](/docs/api/rest/reference/phone/methods/#operation/GetGCP) | `phone:read:call_pickup_group:admin` | +| [Get call pickup group by ID](/docs/api/rest/reference/phone/ma/#operation/GetGCP) | `phone:read:call_pickup_group:master` | +| [Add members to a call pickup group](/docs/api/rest/reference/phone/methods/#operation/addGCPMembers) | `phone:write:call_pickup_group_member:admin` | +| [Add members to a call pickup group](/docs/api/rest/reference/phone/ma/#operation/addGCPMembers) | `phone:write:call_pickup_group_member:master` | +| [Remove members from call pickup group](/docs/api/rest/reference/phone/methods/#operation/removeGCPMembers) | `phone:delete:call_pickup_group_member:admin` | +| [Remove members from call pickup group](/docs/api/rest/reference/phone/ma/#operation/removeGCPMembers) | `phone:delete:call_pickup_group_member:master` | + +### Groups + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get group policy details](/docs/api/rest/reference/phone/methods/#operation/GetGroupPolicyDetails) | `phone:read:group_policy:admin` | +| [Get group policy details](/docs/api/rest/reference/phone/ma/#operation/GetGroupPolicyDetails) | `phone:read:group_policy:master` | +| [Get group phone settings](/docs/api/rest/reference/phone/methods/#operation/getGroupPhoneSettings) | `phone:read:group_setting:admin` | +| [Get group phone settings](/docs/api/rest/reference/phone/ma/#operation/getGroupPhoneSettings) | `phone:read:group_setting:master` | +| [Update group policy](/docs/api/rest/reference/phone/methods/#operation/updateGroupPolicy) | `phone:update:group_policy:admin` | +| [Update group policy](/docs/api/rest/reference/phone/ma/#operation/updateGroupPolicy) | `phone:update:group_policy:master` | + +### IVR + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get auto receptionist IVR](/docs/api/rest/reference/phone/methods/#operation/getAutoReceptionistIVR) | `phone:read:auto_receptionist_ivr:admin` | +| [Get auto receptionist IVR](/docs/api/rest/reference/phone/ma/#operation/getAutoReceptionistIVR) | `phone:read:auto_receptionist_ivr:master` | +| [Update auto receptionist IVR](/docs/api/rest/reference/phone/methods/#operation/updateAutoReceptionistIVR) | `phone:update:auto_receptionist_ivr:admin` | +| [Update auto receptionist IVR](/docs/api/rest/reference/phone/ma/#operation/updateAutoReceptionistIVR) | `phone:update:auto_receptionist_ivr:master` | + +### Inbound Blocked List + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/DeleteExtensiontLevelInboundBlockRules) | `phone:delete:extension_inbound_block_rule:admin`, `phone:delete:extension_inbound_block_rule` | +| [Delete an extension's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/DeleteExtensiontLevelInboundBlockRules) | `phone:delete:extension_inbound_block_rule:master` | +| [Add an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/AddAccountLevelInboundBlockRules) | `phone:write:inbound_block_rule:admin` | +| [Add an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/AddAccountLevelInboundBlockRules) | `phone:write:inbound_block_rule:master` | +| [Delete an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/DeleteAccountLevelInboundBlockRules) | `phone:delete:inbound_block_rule:admin` | +| [Delete an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/DeleteAccountLevelInboundBlockRules) | `phone:delete:inbound_block_rule:master` | +| [Mark a phone number as blocked for all extensions](/docs/api/rest/reference/phone/methods/#operation/MarkPhoneNumberAsBlockedForAllExtensions) | `phone:update:inbound_blocked_for_all:admin` | +| [Mark a phone number as blocked for all extensions](/docs/api/rest/reference/phone/ma/#operation/MarkPhoneNumberAsBlockedForAllExtensions) | `phone:update:inbound_blocked_for_all:master` | +| [List an account's inbound block rules](/docs/api/rest/reference/phone/methods/#operation/ListAccountLevelInboundBlockRules) | `phone:read:list_inbound_block_rules:admin` | +| [List an account's inbound block rules](/docs/api/rest/reference/phone/ma/#operation/ListAccountLevelInboundBlockRules) | `phone:read:list_inbound_block_rules:master` | +| [List an extension's inbound block rules](/docs/api/rest/reference/phone/methods/#operation/ListExtensionLevelInboundBlockRules) | `phone:read:list_extension_inbound_block_rules:admin`, `phone:read:list_extension_inbound_block_rules` | +| [List an extension's inbound block rules](/docs/api/rest/reference/phone/ma/#operation/ListExtensionLevelInboundBlockRules) | `phone:read:list_extension_inbound_block_rules:master` | +| [Delete an account's inbound blocked statistics](/docs/api/rest/reference/phone/methods/#operation/DeleteAccountLevelInboundBlockedStatistics) | `phone:delete:extension_inbound_block_rule_stat:admin` | +| [Delete an account's inbound blocked statistics](/docs/api/rest/reference/phone/ma/#operation/DeleteAccountLevelInboundBlockedStatistics) | `phone:delete:extension_inbound_block_rule_stat:master` | +| [Update an account's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountLevelInboundBlockRule) | `phone:update:inbound_block_rule:admin` | +| [Update an account's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountLevelInboundBlockRule) | `phone:update:inbound_block_rule:master` | +| [Add an extension's inbound block rule](/docs/api/rest/reference/phone/methods/#operation/AddExtensiontLevelInboundBlockRules) | `phone:write:extension_inbound_block_rule:admin`, `phone:write:extension_inbound_block_rule` | +| [Add an extension's inbound block rule](/docs/api/rest/reference/phone/ma/#operation/AddExtensiontLevelInboundBlockRules) | `phone:write:extension_inbound_block_rule:master` | +| [List an account's inbound blocked statistics](/docs/api/rest/reference/phone/methods/#operation/ListAccountLevelInboundBlockedStatistics) | `phone:read:list_extension_inbound_block_rules_stat:admin` | +| [List an account's inbound blocked statistics](/docs/api/rest/reference/phone/ma/#operation/ListAccountLevelInboundBlockedStatistics) | `phone:read:list_extension_inbound_block_rules_stat:master` | + +### Line Keys + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/listLineKeySetting) | `phone:read:line_keys`, `phone:read:line_keys:admin` | +| [Get line key position and settings information](/docs/api/rest/reference/phone/ma/#operation/listLineKeySetting) | `phone:read:line_keys:master` | +| [Delete a line key setting.](/docs/api/rest/reference/phone/methods/#operation/DeleteLineKey) | `phone:delete:line_keys`, `phone:delete:line_keys:admin` | +| [Delete a line key setting.](/docs/api/rest/reference/phone/ma/#operation/DeleteLineKey) | `phone:delete:line_keys:master` | +| [Batch update line key position and settings information](/docs/api/rest/reference/phone/methods/#operation/BatchUpdateLineKeySetting) | `phone:update:line_keys`, `phone:update:line_keys:admin` | +| [Batch update line key position and settings information](/docs/api/rest/reference/phone/ma/#operation/BatchUpdateLineKeySetting) | `phone:update:line_keys:master` | + +### Monitoring Groups + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove all monitors or monitored members from a monitoring group](/docs/api/rest/reference/phone/methods/#operation/removeMembers) | `phone:delete:monitoring_group_member:admin` | +| [Remove all monitors or monitored members from a monitoring group](/docs/api/rest/reference/phone/ma/#operation/removeMembers) | `phone:delete:monitoring_group_member:master` | +| [Get members of a monitoring group](/docs/api/rest/reference/phone/methods/#operation/listMembers) | `phone:read:list_monitoring_group_members:admin` | +| [Get members of a monitoring group](/docs/api/rest/reference/phone/ma/#operation/listMembers) | `phone:read:list_monitoring_group_members:master` | +| [Add members to a monitoring group](/docs/api/rest/reference/phone/methods/#operation/addMembers) | `phone:write:monitoring_group_member:admin` | +| [Add members to a monitoring group](/docs/api/rest/reference/phone/ma/#operation/addMembers) | `phone:write:monitoring_group_member:master` | +| [Delete a monitoring group](/docs/api/rest/reference/phone/methods/#operation/deleteMonitoringGroup) | `phone:delete:monitoring_group:admin` | +| [Delete a monitoring group](/docs/api/rest/reference/phone/ma/#operation/deleteMonitoringGroup) | `phone:delete:monitoring_group:master` | +| [Get monitoring group by ID](/docs/api/rest/reference/phone/methods/#operation/getMonitoringGroupById) | `phone:read:monitoring_group:admin` | +| [Get monitoring group by ID](/docs/api/rest/reference/phone/ma/#operation/getMonitoringGroupById) | `phone:read:monitoring_group:master` | +| [Create a monitoring group](/docs/api/rest/reference/phone/methods/#operation/createMonitoringGroup) | `phone:write:monitoring_group:admin` | +| [Create a monitoring group](/docs/api/rest/reference/phone/ma/#operation/createMonitoringGroup) | `phone:write:monitoring_group:master` | +| [Get a list of monitoring groups on an account](/docs/api/rest/reference/phone/methods/#operation/listMonitoringGroup) | `phone:read:list_monitoring_groups:admin` | +| [Get a list of monitoring groups on an account](/docs/api/rest/reference/phone/ma/#operation/listMonitoringGroup) | `phone:read:list_monitoring_groups:master` | +| [Remove a member from a monitoring group](/docs/api/rest/reference/phone/methods/#operation/removeMember) | `phone:delete:monitoring_group_member:admin` | +| [Remove a member from a monitoring group](/docs/api/rest/reference/phone/ma/#operation/removeMember) | `phone:delete:monitoring_group_member:master` | +| [Update a monitoring group](/docs/api/rest/reference/phone/methods/#operation/updateMonitoringGroup) | `phone:update:monitoring_group:admin` | +| [Update a monitoring group](/docs/api/rest/reference/phone/ma/#operation/updateMonitoringGroup) | `phone:update:monitoring_group:master` | + +### Outbound Calling + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddCommonAreaOutboundCallingExceptionRule) | `phone:write:common_area_outbound_calling_rule:admin` | +| [Add common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddCommonAreaOutboundCallingExceptionRule) | `phone:write:common_area_outbound_calling_rule:master` | +| [Get site level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetSiteOutboundCallingCountriesAndRegions) | `phone:read:site_outbound_calling_rule:admin` | +| [Get site level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetSiteOutboundCallingCountriesAndRegions) | `phone:read:site_outbound_calling_rule:master` | +| [Get account level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetAccountOutboundCallingCountriesAndRegions) | `phone:read:list_outbound_calling_rules:admin` | +| [Get account level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetAccountOutboundCallingCountriesAndRegions) | `phone:read:list_outbound_calling_rules:master` | +| [Update user level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateUserOutboundCallingCountriesOrRegions) | `phone:update:user_outbound_calling_rule:admin` | +| [Update user level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateUserOutboundCallingCountriesOrRegions) | `phone:update:user_outbound_calling_rule:master` | +| [Add user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddUserOutboundCallingExceptionRule) | `phone:write:user_outbound_calling_rule:admin` | +| [Add user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddUserOutboundCallingExceptionRule) | `phone:write:user_outbound_calling_rule:master` | +| [Delete site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteSiteOutboundCallingExceptionRule) | `phone:delete:site_outbound_calling_rule:admin` | +| [Delete site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteSiteOutboundCallingExceptionRule) | `phone:delete:site_outbound_calling_rule:master` | +| [Update site level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateSiteOutboundCallingCountriesOrRegions) | `phone:update:site_outbound_calling_rule:admin` | +| [Update site level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateSiteOutboundCallingCountriesOrRegions) | `phone:update:site_outbound_calling_rule:master` | +| [Delete account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteAccountOutboundCallingExceptionRule) | `phone:delete:outbound_calling_rule:admin` | +| [Delete account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteAccountOutboundCallingExceptionRule) | `phone:delete:outbound_calling_rule:master` | +| [List site level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listSiteOutboundCallingExceptionRule) | `phone:read:site_outbound_calling_rule:admin` | +| [List site level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listSiteOutboundCallingExceptionRule) | `phone:read:site_outbound_calling_rule:master` | +| [Add site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddSiteOutboundCallingExceptionRule) | `phone:write:site_outbound_calling_rule:admin` | +| [Add site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddSiteOutboundCallingExceptionRule) | `phone:write:site_outbound_calling_rule:master` | +| [Update user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateUserOutboundCallingExceptionRule) | `phone:update:user_outbound_calling_rule:admin` | +| [Update user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateUserOutboundCallingExceptionRule) | `phone:update:user_outbound_calling_rule:master` | +| [Update common area level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaOutboundCallingCountriesOrRegions) | `phone:update:common_area_outbound_calling_rule:admin` | +| [Update common area level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaOutboundCallingCountriesOrRegions) | `phone:update:common_area_outbound_calling_rule:master` | +| [Update account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountOutboundCallingExceptionRule) | `phone:update:outbound_calling_rule:admin` | +| [Update account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountOutboundCallingExceptionRule) | `phone:update:outbound_calling_rule:master` | +| [Add account level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/AddAccountOutboundCallingExceptionRule) | `phone:write:outbound_calling_rule:admin` | +| [Add account level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/AddAccountOutboundCallingExceptionRule) | `phone:write:outbound_calling_rule:master` | +| [Update account level outbound calling countries or regions](/docs/api/rest/reference/phone/methods/#operation/UpdateAccountOutboundCallingCountriesOrRegions) | `phone:update:outbound_calling_rule:admin` | +| [Update account level outbound calling countries or regions](/docs/api/rest/reference/phone/ma/#operation/UpdateAccountOutboundCallingCountriesOrRegions) | `phone:update:outbound_calling_rule:master` | +| [Get user level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetUserOutboundCallingCountriesAndRegions) | `phone:read:user_outbound_calling_rule:admin` | +| [Get user level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetUserOutboundCallingCountriesAndRegions) | `phone:read:user_outbound_calling_rule:master` | +| [List account level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listAccountOutboundCallingExceptionRule) | `phone:read:list_outbound_calling_rules:admin` | +| [List account level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listAccountOutboundCallingExceptionRule) | `phone:read:list_outbound_calling_rules:master` | +| [Delete common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteCommonAreaOutboundCallingExceptionRule) | `phone:delete:common_area_outbound_calling_rule:admin` | +| [Delete common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteCommonAreaOutboundCallingExceptionRule) | `phone:delete:common_area_outbound_calling_rule:master` | +| [List common area level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listCommonAreaOutboundCallingExceptionRule) | `phone:read:common_area_outbound_calling_rule:admin` | +| [List common area level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listCommonAreaOutboundCallingExceptionRule) | `phone:read:common_area_outbound_calling_rule:master` | +| [Delete user level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/deleteUserOutboundCallingExceptionRule) | `phone:delete:user_outbound_calling_rule:admin` | +| [Delete user level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/deleteUserOutboundCallingExceptionRule) | `phone:delete:user_outbound_calling_rule:master` | +| [Update common area level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateCommonAreaOutboundCallingExceptionRule) | `phone:update:common_area_outbound_calling_rule:admin` | +| [Update common area level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateCommonAreaOutboundCallingExceptionRule) | `phone:update:common_area_outbound_calling_rule:master` | +| [Get common area level outbound calling countries and regions](/docs/api/rest/reference/phone/methods/#operation/GetCommonAreaOutboundCallingCountriesAndRegions) | `phone:read:common_area_outbound_calling_rule:admin` | +| [Get common area level outbound calling countries and regions](/docs/api/rest/reference/phone/ma/#operation/GetCommonAreaOutboundCallingCountriesAndRegions) | `phone:read:common_area_outbound_calling_rule:master` | +| [Update site level outbound calling exception rule](/docs/api/rest/reference/phone/methods/#operation/UpdateSiteOutboundCallingExceptionRule) | `phone:update:site_outbound_calling_rule:admin` | +| [Update site level outbound calling exception rule](/docs/api/rest/reference/phone/ma/#operation/UpdateSiteOutboundCallingExceptionRule) | `phone:update:site_outbound_calling_rule:master` | +| [List user level outbound calling exception rules](/docs/api/rest/reference/phone/methods/#operation/listUserOutboundCallingExceptionRule) | `phone:read:user_outbound_calling_rule:admin` | +| [List user level outbound calling exception rules](/docs/api/rest/reference/phone/ma/#operation/listUserOutboundCallingExceptionRule) | `phone:read:user_outbound_calling_rule:master` | + +### Phone Devices + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List Smartphones](/docs/api/rest/reference/phone/methods/#operation/ListSmartphones) | `phone:read:list_devices:admin` | +| [List Smartphones](/docs/api/rest/reference/phone/ma/#operation/ListSmartphones) | `phone:read:list_devices:master` | +| [Assign an entity to a device](/docs/api/rest/reference/phone/methods/#operation/addExtensionsToADevice) | `phone:write:device_extension:admin` | +| [Assign an entity to a device](/docs/api/rest/reference/phone/ma/#operation/addExtensionsToADevice) | `phone:write:device_extension:master` | +| [Reboot a desk phone](/docs/api/rest/reference/phone/methods/#operation/rebootPhoneDevice) | `phone:write:reboot_device:admin` | +| [Reboot a desk phone](/docs/api/rest/reference/phone/ma/#operation/rebootPhoneDevice) | `phone:write:reboot_device:master` | +| [Delete a device](/docs/api/rest/reference/phone/methods/#operation/deleteADevice) | `phone:delete:device:admin` | +| [Delete a device](/docs/api/rest/reference/phone/ma/#operation/deleteADevice) | `phone:delete:device:master` | +| [Update provision template of a device](/docs/api/rest/reference/phone/methods/#operation/updateProvisionTemplateToDevice) | `phone:update:device_provision_template:admin` | +| [Update provision template of a device](/docs/api/rest/reference/phone/ma/#operation/updateProvisionTemplateToDevice) | `phone:update:device_provision_template:master` | +| [List devices](/docs/api/rest/reference/phone/methods/#operation/listPhoneDevices) | `phone:read:list_devices:admin` | +| [List devices](/docs/api/rest/reference/phone/ma/#operation/listPhoneDevices) | `phone:read:list_devices:master` | +| [Sync deskphones](/docs/api/rest/reference/phone/methods/#operation/syncPhoneDevice) | `phone:write:sync_device:admin` | +| [Sync deskphones](/docs/api/rest/reference/phone/ma/#operation/syncPhoneDevice) | `phone:write:sync_device:master` | +| [Add a device](/docs/api/rest/reference/phone/methods/#operation/addPhoneDevice) | `phone:write:device:admin` | +| [Add a device](/docs/api/rest/reference/phone/ma/#operation/addPhoneDevice) | `phone:write:device:master` | +| [Get device details](/docs/api/rest/reference/phone/methods/#operation/getADevice) | `phone:read:device:admin` | +| [Get device details](/docs/api/rest/reference/phone/ma/#operation/getADevice) | `phone:read:device:master` | +| [Update a device](/docs/api/rest/reference/phone/methods/#operation/updateADevice) | `phone:update:device:admin` | +| [Update a device](/docs/api/rest/reference/phone/ma/#operation/updateADevice) | `phone:update:device:master` | +| [Unassign an entity from the device](/docs/api/rest/reference/phone/methods/#operation/deleteExtensionFromADevice) | `phone:delete:device_extension:admin` | +| [Unassign an entity from the device](/docs/api/rest/reference/phone/ma/#operation/deleteExtensionFromADevice) | `phone:delete:device_extension:master` | + +### Phone Numbers + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a phone number](/docs/api/rest/reference/phone/methods/#operation/updatePhoneNumberDetails) | `phone:update:number:admin` | +| [Update a phone number](/docs/api/rest/reference/phone/ma/#operation/updatePhoneNumberDetails) | `phone:update:number:master` | +| [Update a site's unassigned phone numbers](/docs/api/rest/reference/phone/methods/#operation/updateSiteForUnassignedPhoneNumbers) | `phone:update:site_number:admin` | +| [Update a site's unassigned phone numbers](/docs/api/rest/reference/phone/ma/#operation/updateSiteForUnassignedPhoneNumbers) | `phone:update:site_number:master` | +| [Assign a phone number to a user](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumber) | `phone:write:user_number`, `phone:write:user_number:admin` | +| [Assign a phone number to a user](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumber) | `phone:write:user_number:master` | +| [Add BYOC phone numbers](/docs/api/rest/reference/phone/methods/#operation/addBYOCNumber) | `phone:write:byo_carrier_number:admin` | +| [Add BYOC phone numbers](/docs/api/rest/reference/phone/ma/#operation/addBYOCNumber) | `phone:write:byo_carrier_number:master` | +| [Delete unassigned phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteUnassignedPhoneNumbers) | `phone:delete:number:admin` | +| [Delete unassigned phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteUnassignedPhoneNumbers) | `phone:delete:number:master` | +| [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/UnassignPhoneNumber) | `phone:delete:user_number`, `phone:delete:user_number:admin` | +| [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/UnassignPhoneNumber) | `phone:delete:user_number:master` | +| [Get a phone number](/docs/api/rest/reference/phone/methods/#operation/getPhoneNumberDetails) | `phone:read:numbers:admin` | +| [Get a phone number](/docs/api/rest/reference/phone/ma/#operation/getPhoneNumberDetails) | `phone:read:numbers:master` | +| [List phone numbers](/docs/api/rest/reference/phone/methods/#operation/listAccountPhoneNumbers) | `phone:read:list_numbers:admin` | +| [List phone numbers](/docs/api/rest/reference/phone/ma/#operation/listAccountPhoneNumbers) | `phone:read:list_numbers:master` | + +### Phone Plans + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List plan information](/docs/api/rest/reference/phone/methods/#operation/listPhonePlans) | `phone:read:list_calling_plans:admin` | +| [List plan information](/docs/api/rest/reference/phone/ma/#operation/listPhonePlans) | `phone:read:list_calling_plans:master` | +| [List calling plans](/docs/api/rest/reference/phone/methods/#operation/listCallingPlans) | `phone:read:list_calling_plans:admin` | +| [List calling plans](/docs/api/rest/reference/phone/ma/#operation/listCallingPlans) | `phone:read:list_calling_plans:master` | + +### Phone Roles + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update a phone role](/docs/api/rest/reference/phone/methods/#operation/UpdatePhoneRole) | `phone:update:role:admin` | +| [Update a phone role](/docs/api/rest/reference/phone/ma/#operation/UpdatePhoneRole) | `phone:update:role:master` | +| [List phone roles](/docs/api/rest/reference/phone/methods/#operation/ListPhoneRoles) | `phone:read:list_roles:admin` | +| [List phone roles](/docs/api/rest/reference/phone/ma/#operation/ListPhoneRoles) | `phone:read:list_roles:master` | +| [Add members to roles](/docs/api/rest/reference/phone/methods/#operation/AddRoleMembers) | `phone:write:role_member:admin` | +| [Add members to roles](/docs/api/rest/reference/phone/ma/#operation/AddRoleMembers) | `phone:write:role_member:master` | +| [Add phone role targets](/docs/api/rest/reference/phone/methods/#operation/AddPhoneRoleTargets) | `phone:write:role:admin` | +| [Add phone role targets](/docs/api/rest/reference/phone/ma/#operation/AddPhoneRoleTargets) | `phone:write:role:master` | +| [Delete phone role targets](/docs/api/rest/reference/phone/methods/#operation/DeletePhoneRoleTargets) | `phone:delete:role:admin` | +| [Delete phone role targets](/docs/api/rest/reference/phone/ma/#operation/DeletePhoneRoleTargets) | `phone:delete:role:master` | +| [List phone role targets](/docs/api/rest/reference/phone/methods/#operation/ListPhoneRoleTargets) | `phone:read:list_roles:admin` | +| [List phone role targets](/docs/api/rest/reference/phone/ma/#operation/ListPhoneRoleTargets) | `phone:read:list_roles:master` | +| [Delete a phone role](/docs/api/rest/reference/phone/methods/#operation/DeletePhoneRole) | `phone:delete:role:admin` | +| [Delete a phone role](/docs/api/rest/reference/phone/ma/#operation/DeletePhoneRole) | `phone:delete:role:master` | +| [Duplicate a phone role](/docs/api/rest/reference/phone/methods/#operation/DuplicatePhoneRole) | `phone:write:role:admin` | +| [Duplicate a phone role](/docs/api/rest/reference/phone/ma/#operation/DuplicatePhoneRole) | `phone:write:role:master` | +| [Delete members in a role](/docs/api/rest/reference/phone/methods/#operation/DelRoleMembers) | `phone:delete:role_member:admin` | +| [Delete members in a role](/docs/api/rest/reference/phone/ma/#operation/DelRoleMembers) | `phone:delete:role_member:master` | +| [List members in a role](/docs/api/rest/reference/phone/methods/#operation/ListRoleMembers) | `phone:read:role_member:admin` | +| [List members in a role](/docs/api/rest/reference/phone/ma/#operation/ListRoleMembers) | `phone:read:role_member:master` | +| [Get role information](/docs/api/rest/reference/phone/methods/#operation/getRoleInformation) | `phone:read:role:admin` | +| [Get role information](/docs/api/rest/reference/phone/ma/#operation/getRoleInformation) | `phone:read:role:master` | + +### Private Directory + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove a member from a private directory](/docs/api/rest/reference/phone/methods/#operation/removeAMemberFromAPrivateDirectory) | `phone:delete:private_directory_member:admin` | +| [Remove a member from a private directory](/docs/api/rest/reference/phone/ma/#operation/removeAMemberFromAPrivateDirectory) | `phone:delete:private_directory_member:master` | +| [Add members to a private directory](/docs/api/rest/reference/phone/methods/#operation/addMembersToAPrivateDirectory) | `phone:write:private_directory_member:admin` | +| [Add members to a private directory](/docs/api/rest/reference/phone/ma/#operation/addMembersToAPrivateDirectory) | `phone:write:private_directory_member:master` | +| [Update a private directory member](/docs/api/rest/reference/phone/methods/#operation/updateAPrivateDirectoryMember) | `phone:update:private_directory_member:admin` | +| [Update a private directory member](/docs/api/rest/reference/phone/ma/#operation/updateAPrivateDirectoryMember) | `phone:update:private_directory_member:master` | +| [List private directory members](/docs/api/rest/reference/phone/methods/#operation/listPrivateDirectoryMembers) | `phone:read:list_private_directory_members:admin` | +| [List private directory members](/docs/api/rest/reference/phone/ma/#operation/listPrivateDirectoryMembers) | `phone:read:list_private_directory_members:master` | + +### Provider Exchange + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List carrier peering phone numbers.](/docs/api/rest/reference/phone/methods/#operation/listCarrierPeeringPhoneNumbers) | `phone:read:list_peering_numbers:admin` | +| [List carrier peering phone numbers.](/docs/api/rest/reference/phone/ma/#operation/listCarrierPeeringPhoneNumbers) | `phone:read:list_peering_numbers:master` | +| [Update peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/updatePeeringPhoneNumbers) | `phone:update:peering_number:admin` | +| [Update peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/updatePeeringPhoneNumbers) | `phone:update:peering_number:master` | +| [Add peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/addPeeringPhoneNumbers) | `phone:write:peering_number:admin` | +| [Add peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/addPeeringPhoneNumbers) | `phone:write:peering_number:master` | +| [List peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/listPeeringPhoneNumbers) | `phone:read:list_peering_numbers:admin` | +| [List peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/listPeeringPhoneNumbers) | `phone:read:list_peering_numbers:master` | +| [Remove peering phone numbers](/docs/api/rest/reference/phone/methods/#operation/deletePeeringPhoneNumbers) | `phone:delete:peering_number:admin` | +| [Remove peering phone numbers](/docs/api/rest/reference/phone/ma/#operation/deletePeeringPhoneNumbers) | `phone:delete:peering_number:master` | + +### Provision Templates + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List provision templates](/docs/api/rest/reference/phone/methods/#operation/listAccountProvisionTemplate) | `phone:read:list_provision_templates:admin` | +| [List provision templates](/docs/api/rest/reference/phone/ma/#operation/listAccountProvisionTemplate) | `phone:read:list_provision_templates:master` | +| [Delete a provision template](/docs/api/rest/reference/phone/methods/#operation/deleteProvisionTemplate) | `phone:delete:provision_template:admin` | +| [Delete a provision template](/docs/api/rest/reference/phone/ma/#operation/deleteProvisionTemplate) | `phone:delete:provision_template:master` | +| [Update a provision template](/docs/api/rest/reference/phone/methods/#operation/updateProvisionTemplate) | `phone:update:provision_template:admin` | +| [Update a provision template](/docs/api/rest/reference/phone/ma/#operation/updateProvisionTemplate) | `phone:update:provision_template:master` | +| [Get a provision template](/docs/api/rest/reference/phone/methods/#operation/GetProvisionTemplate) | `phone:read:provision_template:admin` | +| [Get a provision template](/docs/api/rest/reference/phone/ma/#operation/GetProvisionTemplate) | `phone:read:provision_template:master` | +| [Add a provision template](/docs/api/rest/reference/phone/methods/#operation/addProvisionTemplate) | `phone:write:provision_template:admin` | +| [Add a provision template](/docs/api/rest/reference/phone/ma/#operation/addProvisionTemplate) | `phone:write:provision_template:master` | + +### Recordings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update Auto Delete Field](/docs/api/rest/reference/phone/methods/#operation/UpdateAutoDeleteField) | `phone:update:call_recording`, `phone:update:call_recording:admin` | +| [Update Auto Delete Field](/docs/api/rest/reference/phone/ma/#operation/UpdateAutoDeleteField) | `phone:update:call_recording:master` | +| [Update Recording Status](/docs/api/rest/reference/phone/methods/#operation/UpdateRecordingStatus) | `phone:update:call_recording`, `phone:update:call_recording:admin` | +| [Update Recording Status](/docs/api/rest/reference/phone/ma/#operation/UpdateRecordingStatus) | `phone:update:call_recording:master` | +| [Delete a call recording](/docs/api/rest/reference/phone/methods/#operation/deleteCallRecording) | `phone:delete:call_recording`, `phone:delete:call_recording:admin` | +| [Delete a call recording](/docs/api/rest/reference/phone/ma/#operation/deleteCallRecording) | `phone:delete:call_recording:master` | +| [Get recording by call ID](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordingsByCallIdOrCallLogId) | `phone:read:call_recording`, `phone:read:call_recording:admin` | +| [Get recording by call ID](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordingsByCallIdOrCallLogId) | `phone:read:call_recording:master` | +| [Get user's recordings](/docs/api/rest/reference/phone/methods/#operation/phoneUserRecordings) | `phone:read:list_recordings`, `phone:read:list_recordings:admin` | +| [Get user's recordings](/docs/api/rest/reference/phone/ma/#operation/phoneUserRecordings) | `phone:read:list_recordings:master` | +| [Download a phone recording transcript](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingTranscript) | `phone:read:recording_transcript`, `phone:read:recording_transcript:admin` | +| [Download a phone recording transcript](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingTranscript) | `phone:read:recording_transcript:master` | +| [Download a phone recording](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadRecordingFile) | `phone:read:call_recording`, `phone:read:call_recording:admin` | +| [Download a phone recording](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadRecordingFile) | `phone:read:call_recording:master` | +| [Get call recordings](/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordings) | `phone:read:list_call_recordings:admin` | +| [Get call recordings](/docs/api/rest/reference/phone/ma/#operation/getPhoneRecordings) | `phone:read:list_call_recordings:master` | + +### Reports + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get SMS/MMS charges usage report](/docs/api/rest/reference/phone/methods/#operation/GetSMSChargesUsageReport) | `phone:read:sms_charges:admin` | +| [Get SMS/MMS charges usage report](/docs/api/rest/reference/phone/ma/#operation/GetSMSChargesUsageReport) | `phone:read:sms_charges:master` | +| [Get fax charges usage report](/docs/api/rest/reference/phone/methods/#operation/Getfaxchargesusagereport) | `phone:read:fax_charges:admin` | +| [Get fax charges usage report](/docs/api/rest/reference/phone/ma/#operation/Getfaxchargesusagereport) | `phone:read:fax_charges:master` | +| [Get operation logs report](/docs/api/rest/reference/phone/methods/#operation/getPSOperationLogs) | `phone:read:operation_logs:admin` | +| [Get operation logs report](/docs/api/rest/reference/phone/ma/#operation/getPSOperationLogs) | `phone:read:operation_logs:master` | +| [Get call charges usage report](/docs/api/rest/reference/phone/methods/#operation/GetCallChargesUsageReport) | `phone:read:call_charges:admin` | +| [Get call charges usage report](/docs/api/rest/reference/phone/ma/#operation/GetCallChargesUsageReport) | `phone:read:call_charges:master` | + +### Routing Rules + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/updateRoutingRule) | `phone:update:routing_rule:admin` | +| [Update directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/updateRoutingRule) | `phone:update:routing_rule:master` | +| [List directory backup routing rules](/docs/api/rest/reference/phone/methods/#operation/listRoutingRule) | `phone:read:list_routing_rules:admin` | +| [List directory backup routing rules](/docs/api/rest/reference/phone/ma/#operation/listRoutingRule) | `phone:read:list_routing_rules:master` | +| [Add directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/addRoutingRule) | `phone:write:routing_rule:admin` | +| [Add directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/addRoutingRule) | `phone:write:routing_rule:master` | +| [Get directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/getRoutingRule) | `phone:read:routing_rule:admin` | +| [Get directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/getRoutingRule) | `phone:read:routing_rule:master` | +| [Delete directory backup routing rule](/docs/api/rest/reference/phone/methods/#operation/deleteRoutingRule) | `phone:delete:routing_rule:admin` | +| [Delete directory backup routing rule](/docs/api/rest/reference/phone/ma/#operation/deleteRoutingRule) | `phone:delete:routing_rule:master` | + +### SMS + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/methods/#operation/GetSmsSessions) | `phone:read:sms_session`, `phone:read:sms_session:admin` | +| [List user's SMS sessions in descending order](/docs/api/rest/reference/phone/ma/#operation/GetSmsSessions) | `phone:read:sms_session:master` | +| [Sync SMS by session ID](/docs/api/rest/reference/phone/methods/#operation/smsSessionSync) | `phone:read:sms_session`, `phone:read:sms_session:admin` | +| [Sync SMS by session ID](/docs/api/rest/reference/phone/ma/#operation/smsSessionSync) | `phone:read:sms_session:master` | +| [Get SMS by message ID](/docs/api/rest/reference/phone/methods/#operation/smsByMessageId) | `phone:read:sms_message`, `phone:read:sms_message:admin` | +| [Get SMS by message ID](/docs/api/rest/reference/phone/ma/#operation/smsByMessageId) | `phone:read:sms_message:master` | +| [Get user's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/userSmsSession) | `phone:read:list_sms_sessions`, `phone:read:list_sms_sessions:admin` | +| [Get user's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/userSmsSession) | `phone:read:list_sms_sessions:master` | +| [Get SMS session details](/docs/api/rest/reference/phone/methods/#operation/smsSessionDetails) | `phone:read:sms_session`, `phone:read:sms_session:admin` | +| [Get SMS session details](/docs/api/rest/reference/phone/ma/#operation/smsSessionDetails) | `phone:read:sms_session:master` | +| [Post SMS message](/docs/api/rest/reference/phone/methods/#operation/postSmsMessage) | `phone:read:sms_message`, `phone:read:sms_message:admin` | +| [Get account's SMS sessions](/docs/api/rest/reference/phone/methods/#operation/accountSmsSession) | `phone:read:list_sms_sessions`, `phone:read:list_sms_sessions:admin` | +| [Get account's SMS sessions](/docs/api/rest/reference/phone/ma/#operation/accountSmsSession) | `phone:read:list_sms_sessions:master` | + +### SMS Campaign + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/unassignCampaignPhoneNumber) | `phone:delete:sms_campaign_number:admin` | +| [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/unassignCampaignPhoneNumber) | `phone:delete:sms_campaign_number:master` | +| [List user's opt statuses of phone numbers](/docs/api/rest/reference/phone/methods/#operation/getUserNumberCampaignOptStatus) | `phone:read:sms_campaign_number_opt_status`, `phone:read:sms_campaign_number_opt_status:admin` | +| [List user's opt statuses of phone numbers](/docs/api/rest/reference/phone/ma/#operation/getUserNumberCampaignOptStatus) | `phone:read:sms_campaign_number_opt_status:master` | +| [List opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/getNumberCampaignOptStatus) | `phone:read:sms_campaign_number_opt_status:admin` | +| [List opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/getNumberCampaignOptStatus) | `phone:read:sms_campaign_number_opt_status:master` | +| [Assign a phone number to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/assignCampaignPhoneNumbers) | `phone:write:sms_campaign_number:admin` | +| [Assign a phone number to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/assignCampaignPhoneNumbers) | `phone:write:sms_campaign_number:master` | +| [List SMS campaigns](/docs/api/rest/reference/phone/methods/#operation/listAccountSMSCampaigns) | `phone:read:list_sms_campaigns:admin` | +| [List SMS campaigns](/docs/api/rest/reference/phone/ma/#operation/listAccountSMSCampaigns) | `phone:read:list_sms_campaigns:master` | +| [Update opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/methods/#operation/updateNumberCampaignOptStatus) | `phone:update:sms_campaign_number_opt_status:admin` | +| [Update opt statuses of phone numbers assigned to SMS campaign](/docs/api/rest/reference/phone/ma/#operation/updateNumberCampaignOptStatus) | `phone:update:sms_campaign_number_opt_status:master` | +| [Get an SMS campaign](/docs/api/rest/reference/phone/methods/#operation/GetSMSCampaign) | `phone:read:sms_campaign:admin` | +| [Get an SMS campaign](/docs/api/rest/reference/phone/ma/#operation/GetSMSCampaign) | `phone:read:sms_campaign:master` | + +### SMS Consent + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List opt statuses of phone numbers assigned to SMS consent](/docs/api/rest/reference/phone/methods/#operation/getNumberConsentOptStatus) | `phone:read:sms_consent_number_opt_status:admin` | +| [List opt statuses of phone numbers assigned to SMS consent](/docs/api/rest/reference/phone/ma/#operation/getNumberConsentOptStatus) | `phone:read:sms_consent_number_opt_status:master` | + +### Setting Templates + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a setting template](/docs/api/rest/reference/phone/methods/#operation/addSettingTemplate) | `phone:write:setting_template:admin` | +| [Add a setting template](/docs/api/rest/reference/phone/ma/#operation/addSettingTemplate) | `phone:write:setting_template:master` | +| [Get setting template details](/docs/api/rest/reference/phone/methods/#operation/getSettingTemplate) | `phone:read:setting_template:admin` | +| [Get setting template details](/docs/api/rest/reference/phone/ma/#operation/getSettingTemplate) | `phone:read:setting_template:master` | +| [Update a setting template](/docs/api/rest/reference/phone/methods/#operation/updateSettingTemplate) | `phone:update:setting_template:admin` | +| [Update a setting template](/docs/api/rest/reference/phone/ma/#operation/updateSettingTemplate) | `phone:update:setting_template:master` | +| [List setting templates](/docs/api/rest/reference/phone/methods/#operation/listSettingTemplates) | `phone:read:list_setting_templates:admin` | +| [List setting templates](/docs/api/rest/reference/phone/ma/#operation/listSettingTemplates) | `phone:read:list_setting_templates:master` | + +### Settings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List ported numbers](/docs/api/rest/reference/phone/methods/#operation/listPortedNumbers) | `phone:read:list_ported_numbers:admin` | +| [List ported numbers](/docs/api/rest/reference/phone/ma/#operation/listPortedNumbers) | `phone:read:list_ported_numbers:master` | +| [Update account policy](/docs/api/rest/reference/phone/methods/#operation/updateAccountPolicy) | `phone:update:policy:admin` | +| [Update account policy](/docs/api/rest/reference/phone/ma/#operation/updateAccountPolicy) | `phone:update:policy:master` | +| [List SIP groups](/docs/api/rest/reference/phone/methods/#operation/listSipGroups) | `phone:read:list_sip_groups:admin` | +| [List SIP groups](/docs/api/rest/reference/phone/ma/#operation/listSipGroups) | `phone:read:list_sip_groups:master` | +| [Get ported numbers details](/docs/api/rest/reference/phone/methods/#operation/getPortedNumbersDetails) | `phone:read:ported_number:admin` | +| [Get ported numbers details](/docs/api/rest/reference/phone/ma/#operation/getPortedNumbersDetails) | `phone:read:ported_number:master` | +| [List BYOC SIP trunks](/docs/api/rest/reference/phone/methods/#operation/listBYOCSIPTrunk) | `phone:read:list_sip_trunks:admin` | +| [List BYOC SIP trunks](/docs/api/rest/reference/phone/ma/#operation/listBYOCSIPTrunk) | `phone:read:list_sip_trunks:master` | +| [Update phone account settings](/docs/api/rest/reference/phone/methods/#operation/updatePhoneSettings) | `phone:update:settings:admin` | +| [Update phone account settings](/docs/api/rest/reference/phone/ma/#operation/updatePhoneSettings) | `phone:update:settings:master` | +| [Get account policy details](/docs/api/rest/reference/phone/methods/#operation/GetAccountPolicyDetails) | `phone:read:policy:admin` | +| [Get account policy details](/docs/api/rest/reference/phone/ma/#operation/GetAccountPolicyDetails) | `phone:read:policy:master` | +| [Get phone account settings](/docs/api/rest/reference/phone/methods/#operation/phoneSetting) | `phone:read:settings:admin` | +| [Get phone account settings](/docs/api/rest/reference/phone/ma/#operation/phoneSetting) | `phone:read:settings:master` | + +### Shared Line Appearance + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List shared line appearances](/docs/api/rest/reference/phone/methods/#operation/listSharedLineAppearances) | `phone:read:list_shared_line_appearances:admin` | +| [List shared line appearances](/docs/api/rest/reference/phone/ma/#operation/listSharedLineAppearances) | `phone:read:list_shared_line_appearances:master` | + +### Shared Line Group + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign a member from a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteAMemberSLG) | `phone:delete:shared_line_member:admin` | +| [Unassign a member from a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteAMemberSLG) | `phone:delete:shared_line_member:master` | +| [Assign phone numbers](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumbersSLG) | `phone:write:shared_line_group_number:admin` | +| [Assign phone numbers](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumbersSLG) | `phone:write:shared_line_group_number:master` | +| [Unassign members from a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteMembersOfSLG) | `phone:delete:shared_line_member:admin` | +| [Unassign members from a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteMembersOfSLG) | `phone:delete:shared_line_member:master` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/methods/#operation/deletePhoneNumbersSLG) | `phone:delete:shared_line_group_number:admin` | +| [Unassign all phone numbers](/docs/api/rest/reference/phone/ma/#operation/deletePhoneNumbersSLG) | `phone:delete:shared_line_group_number:master` | +| [Add a policy setting to a shared line group](/docs/api/rest/reference/phone/methods/#operation/addSLGPolicySubSetting) | `phone:write:shared_line_group_policy:admin` | +| [Add a policy setting to a shared line group](/docs/api/rest/reference/phone/ma/#operation/addSLGPolicySubSetting) | `phone:write:shared_line_group_policy:master` | +| [Add members to a shared line group](/docs/api/rest/reference/phone/methods/#operation/addMembersToSharedLineGroup) | `phone:write:shared_line_member:admin` | +| [Add members to a shared line group](/docs/api/rest/reference/phone/ma/#operation/addMembersToSharedLineGroup) | `phone:write:shared_line_member:master` | +| [Update a shared line group policy](/docs/api/rest/reference/phone/methods/#operation/updateSharedLineGroupPolicy) | `phone:update:shared_line_group_policy:admin` | +| [Update a shared line group policy](/docs/api/rest/reference/phone/ma/#operation/updateSharedLineGroupPolicy) | `phone:update:shared_line_group_policy:master` | +| [Update an SLG policy setting](/docs/api/rest/reference/phone/methods/#operation/updateSLGPolicySubSetting) | `phone:update:shared_line_group_policy:admin` | +| [Update an SLG policy setting](/docs/api/rest/reference/phone/ma/#operation/updateSLGPolicySubSetting) | `phone:update:shared_line_group_policy:master` | +| [Update a shared line group](/docs/api/rest/reference/phone/methods/#operation/updateASharedLineGroup) | `phone:update:shared_line_group:admin` | +| [Update a shared line group](/docs/api/rest/reference/phone/ma/#operation/updateASharedLineGroup) | `phone:update:shared_line_group:master` | +| [Unassign a phone number](/docs/api/rest/reference/phone/methods/#operation/deleteAPhoneNumberSLG) | `phone:delete:shared_line_group_number:admin` | +| [Unassign a phone number](/docs/api/rest/reference/phone/ma/#operation/deleteAPhoneNumberSLG) | `phone:delete:shared_line_group_number:master` | +| [Get a shared line group policy](/docs/api/rest/reference/phone/methods/#operation/getSharedLineGroupPolicy) | `phone:read:shared_line_group_policy:admin` | +| [Get a shared line group policy](/docs/api/rest/reference/phone/ma/#operation/getSharedLineGroupPolicy) | `phone:read:shared_line_group_policy:master` | +| [Get a shared line group](/docs/api/rest/reference/phone/methods/#operation/getASharedLineGroup) | `phone:read:shared_line_group:admin` | +| [Get a shared line group](/docs/api/rest/reference/phone/ma/#operation/getASharedLineGroup) | `phone:read:shared_line_group:master` | +| [The list of shared line groups](/docs/api/rest/reference/phone/methods/#operation/listSharedLineGroups) | `phone:read:list_shared_line_groups:admin` | +| [The list of shared line groups](/docs/api/rest/reference/phone/ma/#operation/listSharedLineGroups) | `phone:read:list_shared_line_groups:master` | +| [Delete a shared line group](/docs/api/rest/reference/phone/methods/#operation/deleteASharedLineGroup) | `phone:delete:shared_line_group:admin` | +| [Delete a shared line group](/docs/api/rest/reference/phone/ma/#operation/deleteASharedLineGroup) | `phone:delete:shared_line_group:master` | +| [Delete an SLG policy setting](/docs/api/rest/reference/phone/methods/#operation/removeSLGPolicySubSetting) | `phone:delete:shared_line_group_policy:admin` | +| [Delete an SLG policy setting](/docs/api/rest/reference/phone/ma/#operation/removeSLGPolicySubSetting) | `phone:delete:shared_line_group_policy:master` | +| [Create a shared line group](/docs/api/rest/reference/phone/methods/#operation/createASharedLineGroup) | `phone:write:shared_line_group:admin` | +| [Create a shared line group](/docs/api/rest/reference/phone/ma/#operation/createASharedLineGroup) | `phone:write:shared_line_group:master` | + +### Sites + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a phone site setting](/docs/api/rest/reference/phone/methods/#operation/getSiteSettingForType) | `phone:read:site_setting:admin` | +| [Get a phone site setting](/docs/api/rest/reference/phone/ma/#operation/getSiteSettingForType) | `phone:read:site_setting:master` | +| [Add a site setting](/docs/api/rest/reference/phone/methods/#operation/addSiteSetting) | `phone:write:site_setting:admin` | +| [Add a site setting](/docs/api/rest/reference/phone/ma/#operation/addSiteSetting) | `phone:write:site_setting:master` | +| [Update the site setting](/docs/api/rest/reference/phone/methods/#operation/updateSiteSetting) | `phone:update:site_setting:admin` | +| [Update the site setting](/docs/api/rest/reference/phone/ma/#operation/updateSiteSetting) | `phone:update:site_setting:master` | +| [Delete a phone site](/docs/api/rest/reference/phone/methods/#operation/deletePhoneSite) | `phone:delete:site:admin` | +| [Delete a phone site](/docs/api/rest/reference/phone/ma/#operation/deletePhoneSite) | `phone:delete:site:master` | +| [Create a phone site](/docs/api/rest/reference/phone/methods/#operation/createPhoneSite) | `phone:write:site:admin` | +| [Create a phone site](/docs/api/rest/reference/phone/ma/#operation/createPhoneSite) | `phone:write:site:master` | +| [Add customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/addSiteOutboundCallerNumbers) | `phone:write:site_customized_number:admin` | +| [Add customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/addSiteOutboundCallerNumbers) | `phone:write:site_customized_number:master` | +| [Remove customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteSiteOutboundCallerNumbers) | `phone:delete:site_customized_number:admin` | +| [Remove customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteSiteOutboundCallerNumbers) | `phone:delete:site_customized_number:master` | +| [Get phone site details](/docs/api/rest/reference/phone/methods/#operation/getASite) | `phone:read:site:admin` | +| [Get phone site details](/docs/api/rest/reference/phone/ma/#operation/getASite) | `phone:read:site:master` | +| [Delete a site setting](/docs/api/rest/reference/phone/methods/#operation/deleteSiteSetting) | `phone:delete:site_setting:admin` | +| [Delete a site setting](/docs/api/rest/reference/phone/ma/#operation/deleteSiteSetting) | `phone:delete:site_setting:master` | +| [Update phone site details](/docs/api/rest/reference/phone/methods/#operation/updateSiteDetails) | `phone:update:site:admin` | +| [Update phone site details](/docs/api/rest/reference/phone/ma/#operation/updateSiteDetails) | `phone:update:site:master` | +| [List customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/listSiteCustomizeOutboundCallerNumbers) | `phone:read:list_site_customized_number:admin` | +| [List customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/listSiteCustomizeOutboundCallerNumbers) | `phone:read:list_site_customized_number:master` | +| [List phone sites](/docs/api/rest/reference/phone/methods/#operation/listPhoneSites) | `phone:read:list_sites:admin` | +| [List phone sites](/docs/api/rest/reference/phone/ma/#operation/listPhoneSites) | `phone:read:list_sites:master` | + +### Users + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Unassign user's calling plan](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlan) | `phone:delete:users_calling_plan`, `phone:delete:users_calling_plan:admin` | +| [Unassign user's calling plan](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlan) | `phone:delete:users_calling_plan:master` | +| [Get a user's profile](/docs/api/rest/reference/phone/methods/#operation/phoneUser) | `phone:read:user`, `phone:read:user:admin` | +| [Get a user's profile](/docs/api/rest/reference/phone/ma/#operation/phoneUser) | `phone:read:user:master` | +| [List phone users](/docs/api/rest/reference/phone/methods/#operation/listPhoneUsers) | `phone:read:list_users:admin` | +| [List phone users](/docs/api/rest/reference/phone/ma/#operation/listPhoneUsers) | `phone:read:list_users:master` | +| [Update user's calling plan](/docs/api/rest/reference/phone/methods/#operation/updateCallingPlan) | `phone:update:calling_plan`, `phone:update:calling_plan:admin` | +| [Update user's calling plan](/docs/api/rest/reference/phone/ma/#operation/updateCallingPlan) | `phone:update:calling_plan:master` | +| [Update multiple users' properties in batch](/docs/api/rest/reference/phone/methods/#operation/updateUsersPropertiesInBatch) | `phone:update:batch_users:admin` | +| [Update multiple users' properties in batch](/docs/api/rest/reference/phone/ma/#operation/updateUsersPropertiesInBatch) | `phone:update:batch_users:master` | +| [Assign calling plan to a user](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlan) | `phone:write:calling_plan`, `phone:write:calling_plan:admin` | +| [Assign calling plan to a user](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlan) | `phone:write:calling_plan:master` | +| [Get user policy details](/docs/api/rest/reference/phone/methods/#operation/GetUserPolicyDetails) | `phone:read:user_policy:admin` | +| [Get user policy details](/docs/api/rest/reference/phone/ma/#operation/GetUserPolicyDetails) | `phone:read:user_policy:master` | +| [List users' phone numbers for a customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/listUserCustomizeOutboundCallerNumbers) | `phone:read:list_user_customized_number`, `phone:read:list_user_customized_number:admin` | +| [List users' phone numbers for a customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/listUserCustomizeOutboundCallerNumbers) | `phone:read:list_user_customized_number:master` | +| [Get a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/phoneUserSettings) | `phone:read:user_setting:admin`, `phone:read:user_setting` | +| [Get a user's profile settings](/docs/api/rest/reference/phone/ma/#operation/phoneUserSettings) | `phone:read:user_setting:master` | +| [Update user policy](/docs/api/rest/reference/phone/methods/#operation/updateUserPolicy) | `phone:update:user_policy:admin` | +| [Update user policy](/docs/api/rest/reference/phone/ma/#operation/updateUserPolicy) | `phone:update:user_policy:master` | +| [Remove users' customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/methods/#operation/deleteUserOutboundCallerNumbers) | `phone:delete:user_customized_number`, `phone:delete:user_customized_number:admin` | +| [Remove users' customized outbound caller ID phone numbers](/docs/api/rest/reference/phone/ma/#operation/deleteUserOutboundCallerNumbers) | `phone:delete:user_customized_number:master` | +| [Update a user's profile settings](/docs/api/rest/reference/phone/methods/#operation/updateUserSettings) | `phone:update:user_setting`, `phone:update:user_setting:admin` | +| [Update a user's profile settings](/docs/api/rest/reference/phone/ma/#operation/updateUserSettings) | `phone:update:user_setting:master` | +| [Add a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/addUserSetting) | `phone:write:shared_setting`, `phone:write:shared_setting:admin` | +| [Add a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/addUserSetting) | `phone:write:shared_setting:master` | +| [Batch add users](/docs/api/rest/reference/phone/methods/#operation/batchAddUsers) | `phone:write:batch_users:admin` | +| [Batch add users](/docs/api/rest/reference/phone/ma/#operation/batchAddUsers) | `phone:write:batch_users:master` | +| [Update a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/updateUserSetting) | `phone:update:shared_setting`, `phone:update:shared_setting:admin` | +| [Update a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/updateUserSetting) | `phone:update:shared_setting:master` | +| [Delete a user's shared access setting](/docs/api/rest/reference/phone/methods/#operation/deleteUserSetting) | `phone:delete:shared_setting`, `phone:delete:shared_setting:admin` | +| [Delete a user's shared access setting](/docs/api/rest/reference/phone/ma/#operation/deleteUserSetting) | `phone:delete:shared_setting:master` | +| [Update a user's profile](/docs/api/rest/reference/phone/methods/#operation/updateUserProfile) | `phone:update:user`, `phone:update:user:admin` | +| [Update a user's profile](/docs/api/rest/reference/phone/ma/#operation/updateUserProfile) | `phone:update:user:master` | +| [Add phone numbers for users' customized outbound caller ID](/docs/api/rest/reference/phone/methods/#operation/addUserOutboundCallerNumbers) | `phone:write:user_customized_number`, `phone:write:user_customized_number:admin` | +| [Add phone numbers for users' customized outbound caller ID](/docs/api/rest/reference/phone/ma/#operation/addUserOutboundCallerNumbers) | `phone:write:user_customized_number:master` | + +### Voicemails + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get user's voicemails](/docs/api/rest/reference/phone/methods/#operation/phoneUserVoiceMails) | `phone:read:list_voicemails`, `phone:read:list_voicemails:admin` | +| [Get user's voicemails](/docs/api/rest/reference/phone/ma/#operation/phoneUserVoiceMails) | `phone:read:list_voicemails:master` | +| [Update Voicemail Read Status](/docs/api/rest/reference/phone/methods/#operation/updateVoicemailReadStatus) | `phone:update:voicemail`, `phone:update:voicemail:admin` | +| [Update Voicemail Read Status](/docs/api/rest/reference/phone/ma/#operation/updateVoicemailReadStatus) | `phone:update:voicemail:master` | +| [Get account voicemails](/docs/api/rest/reference/phone/methods/#operation/accountVoiceMails) | `phone:read:list_voicemails:admin` | +| [Get account voicemails](/docs/api/rest/reference/phone/ma/#operation/accountVoiceMails) | `phone:read:list_voicemails:master` | +| [Get user voicemail details from a call log](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetailsByCallIdOrCallLogId) | `phone:read:voicemail`, `phone:read:voicemail:admin` | +| [Get user voicemail details from a call log](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetailsByCallIdOrCallLogId) | `phone:read:voicemail:master` | +| [Get voicemail details](/docs/api/rest/reference/phone/methods/#operation/getVoicemailDetails) | `phone:read:voicemail`, `phone:read:voicemail:admin` | +| [Get voicemail details](/docs/api/rest/reference/phone/ma/#operation/getVoicemailDetails) | `phone:read:voicemail:master` | +| [Download a phone voicemail](/docs/api/rest/reference/phone/methods/#operation/phoneDownloadVoicemailFile) | `phone:read:voicemail:admin`, `phone:read:voicemail` | +| [Download a phone voicemail](/docs/api/rest/reference/phone/ma/#operation/phoneDownloadVoicemailFile) | `phone:read:voicemail:master` | +| [Delete a voicemail](/docs/api/rest/reference/phone/methods/#operation/deleteVoicemail) | `phone:delete:voicemail`, `phone:delete:voicemail:admin` | +| [Delete a voicemail](/docs/api/rest/reference/phone/ma/#operation/deleteVoicemail) | `phone:delete:voicemail:master` | + +### Zoom Rooms + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Remove a phone number from a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/UnassignPhoneNumberFromZoomRoom) | `phone:delete:room_phone_number:admin` | +| [Remove a phone number from a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/UnassignPhoneNumberFromZoomRoom) | `phone:delete:room_phone_number:master` | +| [Assign calling plans to a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/assignCallingPlanToRoom) | `phone:write:room_calling_plan:admin` | +| [Assign calling plans to a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/assignCallingPlanToRoom) | `phone:write:room_calling_plan:master` | +| [Add a Zoom Room to a Zoom Phone](/docs/api/rest/reference/phone/methods/#operation/addZoomRoom) | `phone:write:room:admin` | +| [Add a Zoom Room to a Zoom Phone](/docs/api/rest/reference/phone/ma/#operation/addZoomRoom) | `phone:write:room:master` | +| [Update a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/updateZoomRoom) | `phone:update:room:admin` | +| [Update a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/updateZoomRoom) | `phone:update:room:master` | +| [Assign phone numbers to a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/assignPhoneNumberToZoomRoom) | `phone:write:room_phone_number:admin` | +| [Assign phone numbers to a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/assignPhoneNumberToZoomRoom) | `phone:write:room_phone_number:master` | +| [Remove a calling plan from a Zoom Room](/docs/api/rest/reference/phone/methods/#operation/unassignCallingPlanFromRoom) | `phone:delete:room_calling_plan:admin` | +| [Remove a calling plan from a Zoom Room](/docs/api/rest/reference/phone/ma/#operation/unassignCallingPlanFromRoom) | `phone:delete:room_calling_plan:master` | +| [List Zoom Rooms without Zoom Phone assignment](/docs/api/rest/reference/phone/methods/#operation/listUnassignedZoomRooms) | `phone:read:list_rooms:admin` | +| [List Zoom Rooms without Zoom Phone assignment](/docs/api/rest/reference/phone/ma/#operation/listUnassignedZoomRooms) | `phone:read:list_rooms:master` | +| [Remove a Zoom Room from a ZP account](/docs/api/rest/reference/phone/methods/#operation/RemoveZoomRoom) | `phone:delete:room:admin` | +| [Remove a Zoom Room from a ZP account](/docs/api/rest/reference/phone/ma/#operation/RemoveZoomRoom) | `phone:delete:room:master` | +| [Get a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/getZoomRoom) | `phone:read:room:admin` | +| [Get a Zoom Room under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/getZoomRoom) | `phone:read:room:master` | +| [List Zoom Rooms under Zoom Phone license](/docs/api/rest/reference/phone/methods/#operation/listZoomRooms) | `phone:read:list_rooms:admin` | +| [List Zoom Rooms under Zoom Phone license](/docs/api/rest/reference/phone/ma/#operation/listZoomRooms) | `phone:read:list_rooms:master` | + +## Zoom Revenue Accelerator + + +### Accounts + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get indicators settings [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/accountSettingsIndicatorsDeprecated) | `zra:read:indicator`, `zra:read:indicator:admin` | +| [Get indicators settings](/docs/api/rest/reference/iq/methods/#operation/accountIndicatorsSettings) | `zra:read:indicator`, `zra:read:indicator:admin` | + +### Conversations + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Upload iq multipart file.](/docs/api/rest/reference/iq/methods/#operation/UploadZraMultipartFile.) | `zra:write:file`, `zra:write:file:admin` | +| [Get conversation comments [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsDeprecated) | `zra:read:list_conversation_comments`, `zra:read:list_conversation_comments:admin` | +| [Delete conversation's comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentDeprecated) | `zra:delete:conversation_comment`, `zra:delete:conversation_comment:admin` | +| [Add new comments to the conversation [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationCommentDeprecated) | `zra:write:conversation_comment`, `zra:write:conversation_comment:admin` | +| [Add new comments to the conversation](/docs/api/rest/reference/iq/methods/#operation/addConversationComments) | `zra:read:list_conversation_comments`, `zra:read:list_conversation_comments:admin` | +| [Get conversation content analysis [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisDeprecated) | `zra:read:conversation_analysis`, `zra:read:conversation_analysis:admin` | +| [Get a user's playlist [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylistDeprecated) | `zra:read:list_conversation_playlists` | +| [Get conversation comments](/docs/api/rest/reference/iq/methods/#operation/getConversationCommentsById) | `zra:read:list_conversation_comments`, `zra:read:list_conversation_comments:admin` | +| [List conversations [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listConversationsDeprecated) | `zra:read:list_conversations`, `zra:read:list_conversations:admin` | +| [Update conversation host id to new host id by conversation id [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UpdateconversationhostidtonewhostidbyconversationidDeprecated) | `zra:update:conversation_host`, `zra:update:conversation_host:admin` | +| [Delete conversation by conversation ID [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/deleteConversationDeprecated) | `zra:delete:conversations`, `zra:delete:conversations:admin` | +| [Get conversation content analysis](/docs/api/rest/reference/iq/methods/#operation/getConversationContentAnalysisById) | `zra:read:conversation_analysis`, `zra:read:conversation_analysis:admin` | +| [Update conversation host id to new host id by conversation id](/docs/api/rest/reference/iq/methods/#operation/UpdateConversationhostid2NewHostidByConversationId) | `zra:update:conversation_host`, `zra:update:conversation_host:admin` | +| [Add conversation by file id or download url.](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrl) | `zra:write:conversation`, `zra:write:conversation:admin` | +| [Delete conversation's comment](/docs/api/rest/reference/iq/methods/#operation/deleteConversationCommentById) | `zra:delete:conversation_comment`, `zra:delete:conversation_comment:admin` | +| [Upload iq multipart file. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIqMultipartFileDeprecated) | `zra:write:file`, `zra:write:file:admin` | +| [Get conversation information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInfoDeprecated) | `zra:read:conversations:admin`, `zra:read:conversations` | +| [Add conversation by meeting record url or meeting UUID.](/docs/api/rest/reference/iq/methods/#operation/addConversationByRecord) | `zra:write:conversation`, `zra:write:conversation:admin` | +| [Upload IQ file](/docs/api/rest/reference/iq/methods/#operation/UploadZraFile) | `zra:write:file`, `zra:write:file:admin` | +| [Add conversation by file id or download url. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/AddConversationByFileIdOrDownloadUrlDeprecated) | `zra:write:conversation`, `zra:write:conversation:admin` | +| [Edit conversation comment [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentDeprecated) | `zra:update:conversation_comment`, `zra:update:conversation_comment:admin` | +| [Get conversation scorecards [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsDeprecated) | `zra:read:conversation_scorecards`, `zra:read:conversation_scorecards:admin` | +| [Upload IQ file [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/UploadIQFileDeprecated) | `zra:write:file`, `zra:write:file:admin` | +| [Initiate and complete a multipart upload.](/docs/api/rest/reference/iq/methods/#operation/InitiateAndCompleteAMultipartUpload) | `zra:write:file`, `zra:write:file:admin` | +| [Get a user's playlist](/docs/api/rest/reference/iq/methods/#operation/getUserPlaylists) | `zra:read:list_conversation_playlists` | +| [List conversations](/docs/api/rest/reference/iq/methods/#operation/listAllConversations) | `zra:read:list_conversations`, `zra:read:list_conversations:admin` | +| [Get conversation interactions](/docs/api/rest/reference/iq/methods/#operation/getConversationInteraction) | `zra:read:conversation_participants`, `zra:read:conversation_participants:admin` | +| [Get conversation information](/docs/api/rest/reference/iq/methods/#operation/getConversationDetail) | `zra:read:conversations:admin`, `zra:read:conversations` | +| [Edit conversation comment](/docs/api/rest/reference/iq/methods/#operation/editConversationCommentById) | `zra:update:conversation_comment`, `zra:update:conversation_comment:admin` | +| [Delete conversation by conversation ID](/docs/api/rest/reference/iq/methods/#operation/deleteConversationById) | `zra:delete:conversations`, `zra:delete:conversations:admin` | +| [Get conversation scorecards](/docs/api/rest/reference/iq/methods/#operation/getConversationScorecardsById) | `zra:read:conversation_scorecards`, `zra:read:conversation_scorecards:admin` | +| [Get conversation interactions [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getConversationInteractionsDeprecated) | `zra:read:conversation_participants`, `zra:read:conversation_participants:admin` | +| [Add conversation by meeting record url or meeting UUID. [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/addConversationDeprecated) | `zra:write:conversation`, `zra:write:conversation:admin` | + +### Deals + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List deals](/docs/api/rest/reference/iq/methods/#operation/listAllDeals) | `zra:read:list_deals`, `zra:read:list_deals:admin` | +| [Delete activity from the deal](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromDeal) | `zra:delete:deal_activity`, `zra:delete:deal_activity:admin` | +| [Get deal information [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealInfoDeprecated) | `zra:read:deal`, `zra:read:deal:admin` | +| [Get deal activities](/docs/api/rest/reference/iq/methods/#operation/geAllActivitiesFromDeal) | `zra:read:list_deal_activities`, `zra:read:list_deal_activities:admin` | +| [Get deal activities [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/getDealActivitiesDeprecated) | `zra:read:list_deal_activities`, `zra:read:list_deal_activities:admin` | +| [List deals [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/listDealsDeprecated) | `zra:read:list_deals`, `zra:read:list_deals:admin` | +| [Delete activity from the deal [Deprecated]](/docs/api/rest/reference/iq/methods/#operation/DeleteActivityFromTheDealDeprecated) | `zra:delete:deal_activity`, `zra:delete:deal_activity:admin` | +| [Get deal information](/docs/api/rest/reference/iq/methods/#operation/getDealDetail) | `zra:read:deal`, `zra:read:deal:admin` | + +### ScheduleMeetings + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List scheduled meetings](/docs/api/rest/reference/iq/methods/#operation/Listallscheduledmeetings) | `zra:read:list_conversations`, `zra:read:list_conversations:admin` | + +### Teams + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete Team](/docs/api/rest/reference/iq/methods/#operation/DeleteTeam) | `zra:delete:team:admin` | +| [Get Team Detail](/docs/api/rest/reference/iq/methods/#operation/GetTeamDetail) | `zra:read:team:admin` | +| [Assign Team Managers](/docs/api/rest/reference/iq/methods/#operation/AssignTeamManagers) | `zra:write:team_manages:admin` | +| [Assign Team Members](/docs/api/rest/reference/iq/methods/#operation/AssignTeamMembers) | `zra:write:team_members:admin` | +| [List Unassigned Team Users](/docs/api/rest/reference/iq/methods/#operation/ListUnassignedTeamUsers) | `zra:read:unassigned_team_users:admin` | +| [Move team to new parent](/docs/api/rest/reference/iq/methods/#operation/MoveTeam) | `zra:update:team:admin` | +| [Remove additional access from current team](/docs/api/rest/reference/iq/methods/#operation/DeleteSharedFromTeams) | `zra:delete:team:admin` | +| [Update Team name](/docs/api/rest/reference/iq/methods/#operation/UpdateTeam) | `zra:update:team:admin` | +| [Unassign Team Members](/docs/api/rest/reference/iq/methods/#operation/UnassignTeamMembers) | `zra:delete:team_members:admin` | +| [List Team Managers](/docs/api/rest/reference/iq/methods/#operation/ListTeamManagers) | `zra:read:team_managers:admin` | +| [Create Team](/docs/api/rest/reference/iq/methods/#operation/CreateTeam) | `zra:write:team:admin` | +| [Grant additional access to current team](/docs/api/rest/reference/iq/methods/#operation/AddSharedFromTeams) | `zra:write:team:admin` | +| [List Account Teams](/docs/api/rest/reference/iq/methods/#operation/ListTeams) | `zra:read:team_list:admin` | +| [Remove additional access from target teams](/docs/api/rest/reference/iq/methods/#operation/DeleteSharedToTeams) | `zra:delete:team:admin` | +| [Unassign Team Managers](/docs/api/rest/reference/iq/methods/#operation/UnassignTeamManagers) | `zra:delete:team_managers:admin` | +| [List Team Members](/docs/api/rest/reference/iq/methods/#operation/ListTeamMembers) | `zra:read:team_members:admin` | +| [Grant additional access to target teams](/docs/api/rest/reference/iq/methods/#operation/AddSharedToTeams) | `zra:write:team:admin` | + +## Zoom Rooms + + +### Room Apps + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Config Zoom Room Controller Apps](/docs/api/rest/reference/zoom-rooms/methods/#operation/ConfigZoomRoomControllerApps) | `zoom_rooms:write:zoom_apps:admin` | + +### Visitor Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a list of visitors by location](/docs/api/rest/reference/zoom-rooms/methods/#operation/invitationList) | `visitor_management:read:list_invitations`, `visitor_management:read:list_invitations:admin` | +| [Invitation details by invitationID](/docs/api/rest/reference/zoom-rooms/methods/#operation/getInvitation) | `visitor_management:read:invitation_details`, `visitor_management:read:invitation_details:admin` | +| [Update an invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateInvitation) | `visitor_management:update:invitation`, `visitor_management:update:invitation:admin` | +| [Send an invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/createInvitation) | `visitor_management:write:invitation`, `visitor_management:write:invitation:admin` | +| [Check in a visitor](/docs/api/rest/reference/zoom-rooms/methods/#operation/checkinVisitor) | `visitor_management:write:check_in`, `visitor_management:write:check_in:admin` | +| [Delete an Invitation](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteInvitation) | `visitor_management:delete:invitation`, `visitor_management:delete:invitation:admin` | + +### Workspaces + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get a workspace's reservations](/docs/api/rest/reference/zoom-rooms/methods/#operation/listReservations) | `workspace:read:list_reservations`, `workspace:read:list_reservations:admin` | +| [Get a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorkspace) | `workspace:read:list_workspaces`, `workspace:read:list_workspaces:admin` | +| [Add or Update a Workspace floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddOrUpdateAWorkspaceFloorMap) | `workspace:write:location`, `workspace:write:location:admin` | +| [Update a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateReservation) | `workspace:update:reservation`, `workspace:update:reservation:admin` | +| [Edit a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/PatchWorkspaceasset) | `workspace:update:asset:admin` | +| [Delete Workspace floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteWorkspaceFloorMap) | `workspace:write:location`, `workspace:write:location:admin` | +| [List workspace reservation questionnaires](/docs/api/rest/reference/zoom-rooms/methods/#operation/Listworkspacereservationquestionnaires) | `workspace:read:reservation:admin`, `workspace:read:reservation:master` | +| [Create a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/createReservation) | `workspace:write:reservation`, `workspace:write:reservation:admin` | +| [List workspace additional information with time range](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getaworkspaceadditionalenhancements) | `workspace:read:list_workspaces`, `workspace:read:list_workspaces:admin` | +| [Update workspace settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateWorkspaceSettings) | `workspace:update:setting:admin`, `workspace:update:setting:master` | +| [Check in/out of a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/reservationEvent) | `workspace:write:events`, `workspace:write:events:admin` | +| [Get all workspace assets](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getallworkspaceassets) | `workspace:read:asset:admin` | +| [Create a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddaWorkspaceasset) | `workspace:write:asset:admin` | +| [Get a workspace reservation by reservationId](/docs/api/rest/reference/zoom-rooms/methods/#operation/GETGetaworkspacereservationbyreservationID) | `workspace:read:reservation`, `workspace:read:reservation:admin` | +| [List released workspaces by timeout](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorksapceReservationReleaseInof) | `zoom_rooms:read:list_release_infos:admin` | +| [Get Workspace Calendar Free/Busy Event](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetWorkspaceCalendarFree/BusyEvent) | `workspace:read:workspace:admin` | +| [List workspaces](/docs/api/rest/reference/zoom-rooms/methods/#operation/listWorkspaces) | `workspace:read:list_workspaces`, `workspace:read:list_workspaces:admin` | +| [Create a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/createWorkspace) | `workspace:write:workspace:admin` | +| [Delete a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteWorkspace) | `workspace:delete:workspace:admin` | +| [Update a workspace](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateWorkspace) | `workspace:update:workspace:admin` | +| [Get a workspace QR code](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWorkspaceQRCode) | `workspace:read:qr:admin` | +| [Delete a reservation](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteReservation) | `workspace:delete:reservation`, `workspace:delete:reservation:admin` | +| [Get a location's hot desk usage](/docs/api/rest/reference/zoom-rooms/methods/#operation/getHotDeskUsage) | `workspace:read:usage`, `workspace:read:usage:admin` | +| [Get a user's workspace's reservations](/docs/api/rest/reference/zoom-rooms/methods/#operation/userListReservations) | `workspace:read:list_reservations:admin` | +| [Get a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/get_workspace_asset) | `workspace:read:asset:admin` | +| [Set Workspace Calendar Free/Busy Event](/docs/api/rest/reference/zoom-rooms/methods/#operation/SetCalendarFree/BusyEvent) | `workspace:write:workspace:admin` | +| [Get a workspace location floor map](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getlocationfloormap) | `workspace:read:location`, `workspace:read:location:admin` | +| [Delete a workspace asset](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteaWorkspaceasset) | `workspace:delete:asset:admin` | +| [Set a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/setADeskAssignment) | `workspace:write:assignment:admin` | +| [Get a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getadeskassignment) | `workspace:read:assignment`, `workspace:read:assignment:admin` | +| [Delete a desk assignment](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadeskassignment) | `workspace:delete:assignment:admin` | + +### Zoom Rooms + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get device information](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomDevices) | `zoom_rooms:read:device_profile:admin` | +| [Get Zoom Room settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRSettings) | `zoom_rooms:read:room_settings:admin` | +| [Get Zoom Room settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRSettings) | `zoom_rooms:read:room_settings:master` | +| [List Zoom Rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZoomRooms) | `zoom_rooms:read:list_rooms:admin` | +| [List Zoom Rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZoomRooms) | `zoom_rooms:read:list_rooms:master` | +| [Delete a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteRoomProfile) | `zoom_rooms:delete:device_profile:admin` | +| [Delete a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteRoomProfile) | `zoom_rooms:delete:device_profile:master` | +| [Use Zoom Room controls](/docs/api/rest/reference/zoom-rooms/methods/#operation/ZoomRoomsControls) | `zoom_rooms:update:room_control:admin` | +| [Use Zoom Room controls](/docs/api/rest/reference/zoom-rooms/ma/#operation/ZoomRoomsControls) | `zoom_rooms:update:room_control:master` | +| [Create a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/createRoomDeviceProfile) | `zoom_rooms:write:device_profile:admin` | +| [Create a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/createRoomDeviceProfile) | `zoom_rooms:write:device_profile:master` | +| [Update Zoom Room settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRSettings) | `zoom_rooms:update:room_settings:admin` | +| [Update Zoom Room settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRSettings) | `zoom_rooms:update:room_settings:master` | +| [List Zoom Room devices](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZRDevices) | `zoom_rooms:read:list_devices:admin` | +| [List Zoom Room devices](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZRDevices) | `zoom_rooms:read:list_devices:master` | +| [Update E911 digital signage](/docs/api/rest/reference/zoom-rooms/methods/#operation/manageE911signage) | `zoom_rooms:update:room_controls:admin` | +| [Update E911 digital signage](/docs/api/rest/reference/zoom-rooms/ma/#operation/manageE911signage) | `zoom_rooms:update:room_controls:master` | +| [Get Zoom Room profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRProfile) | `zoom_rooms:read:room:admin` | +| [Get Zoom Room profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRProfile) | `zoom_rooms:read:room:master` | +| [Delete a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteAZoomRoom) | `zoom_rooms:delete:room:admin` | +| [Delete a Zoom Room](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteAZoomRoom) | `zoom_rooms:delete:room:master` | +| [Get Zoom Rooms virtual controller URL](/docs/api/rest/reference/zoom-rooms/methods/#operation/getWebzrcUrl) | `zoom_rooms:read:virtual_controller:admin` | +| [Get Zoom Rooms virtual controller URL](/docs/api/rest/reference/zoom-rooms/ma/#operation/getWebzrcUrl) | `zoom_rooms:read:virtual_controller:admin` | +| [Update a Zoom Room profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateRoomProfile) | `zoom_rooms:update:room:admin` | +| [Update a Zoom Room profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateRoomProfile) | `zoom_rooms:update:room:master` | +| [Change a Zoom Room's location](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeZRLocation) | `zoom_rooms:update:room_location:admin` | +| [Change a Zoom Room's location](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeZRLocation) | `zoom_rooms:update:room_location:master` | +| [List device profiles](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomProfiles) | `zoom_rooms:read:list_device_profiles:admin` | +| [List device profiles](/docs/api/rest/reference/zoom-rooms/ma/#operation/getRoomProfiles) | `zoom_rooms:read:list_device_profiles:master` | +| [Add a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/addARoom) | `zoom_rooms:write:room:admin` | +| [Add a Zoom Room](/docs/api/rest/reference/zoom-rooms/ma/#operation/addARoom) | `zoom_rooms:write:room:master` | +| [Get Zoom Room sensor data](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRSensorData) | `zoom_rooms:read:sensor_data:admin` | +| [Update a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateDeviceProfile) | `zoom_rooms:update:device_profile:admin` | +| [Update a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateDeviceProfile) | `zoom_rooms:update:device_profile:master` | +| [Get a device profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getRoomProfile) | `zoom_rooms:read:device_profile:admin` | +| [Get a device profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getRoomProfile) | `zoom_rooms:read:device_profile:master` | + +### Zoom Rooms Account + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRAccountProfile) | `zoom_rooms:read:account_profile:admin` | +| [Get Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRAccountProfile) | `zoom_rooms:read:account_profile:master` | +| [Update Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRAccProfile) | `zoom_rooms:update:account_profile:admin` | +| [Update Zoom Room account profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRAccProfile) | `zoom_rooms:update:account_profile:master` | +| [Get Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRAccountSettings) | `zoom_rooms:read:account_settings:admin` | +| [Get Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRAccountSettings) | `zoom_rooms:read:account_settings:master` | +| [Update Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZoomRoomAccSettings) | `zoom_rooms:update:account_settings:admin` | +| [Update Zoom Room account settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZoomRoomAccSettings) | `zoom_rooms:update:account_settings:master` | + +### Zoom Rooms Calendar + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Start calendar service sync process](/docs/api/rest/reference/zoom-rooms/methods/#operation/syncACalendarService) | `zoom_rooms:update:calendar_service:admin` | +| [List calendar services](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarServices) | `zoom_rooms:read:list_calendar_services:admin` | +| [List calendar resources by calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarResourcesByServiceId) | `zoom_rooms:read:list_calendar_resources:admin` | +| [Add a calendar resource to a calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/addACalendarResourceToCalendarService) | `zoom_rooms:write:calendar_resource:admin` | +| [Delete a calendar resource](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteACalendarResource) | `zoom_rooms:delete:calendar_resource:admin` | +| [Get a calendar resource by ID](/docs/api/rest/reference/zoom-rooms/methods/#operation/getCalendarResourceById) | `zoom_rooms:read:calendar_resource:admin` | +| [Delete a calendar service](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteACalendarService) | `zoom_rooms:delete:calendar_service:admin` | + +### Zoom Rooms Content + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDigitalSignagelibraryplaylistpublishedrooms) | `zoom_rooms:read:digital_signage_library_playlists:admin` | +| [List Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/ListDigitalSignagelibraryplaylistpublishedrooms) | `zoom_rooms:read:digital_signage_library_playlists:master` | +| [Add Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddZoomRoomsBackgroundImageLibraryContent) | `zoom_rooms:write:background_library_content:admin` | +| [Update Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateaZoomRoomsBackgroundLibraryFolderName) | `zoom_rooms:update:background_library_folder:admin` | +| [List default Zoom Rooms background image library contents](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDefaultZoomRoomsBackgroundImageLibrarycontents) | `zoom_rooms:read:background_library_content:admin` | +| [Add a digital signage URL](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddadigitalsignageURL) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Add a digital signage URL](/docs/api/rest/reference/zoom-rooms/ma/#operation/AddadigitalsignageURL) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Update Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateDigitalSignagelibraryplaylistpublishedrooms) | `zoom_rooms:write:digital_signage_library_playlists:admin` | +| [Update Digital Signage library playlist published rooms](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateDigitalSignagelibraryplaylistpublishedrooms) | `zoom_rooms:write:digital_signage_library_playlists:master` | +| [Add Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddZoomRoomsBackgroundLibraryFolder) | `zoom_rooms:write:background_library_folder:admin` | +| [Delete Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteZoomRoomsBackgroundLibraryFolder) | `zoom_rooms:delete:background_library_folder:admin` | +| [Add a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/AddaDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:admin` | +| [Add a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/AddaDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:master` | +| [Delete Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:admin` | +| [Delete Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/DeleteDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:master` | +| [Update Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateDigitalSignagelibraryplaylistcontentitems) | `zoom_rooms:write:digital_signage_library_playlists:admin` | +| [Update Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateDigitalSignagelibraryplaylistcontentitems) | `zoom_rooms:write:digital_signage_library_playlists:master` | +| [Get Zoom Rooms Background Image Library Folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetZoomRoomsBackgroundLibraryFolder) | `zoom_rooms:read:background_library_folder:admin` | +| [Update a Digital Signage content item attributes](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignagecontentitemattributes) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Update a Digital Signage content item attributes](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignagecontentitemattributes) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [List Zoom Rooms Background Image Library Folders](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListZoomRoomsBackgroundLibraryFolders) | `zoom_rooms:read:background_library_folder:admin` | +| [List Digital Signage library playlists](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListDigitalSignagelibraryplaylists) | `zoom_rooms:read:digital_signage_library_playlists:admin` | +| [List Digital Signage library playlists](/docs/api/rest/reference/zoom-rooms/ma/#operation/ListDigitalSignagelibraryplaylists) | `zoom_rooms:read:digital_signage_library_playlists:master` | +| [Update a digital signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Update a digital signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Add a digital signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Addadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Add a digital signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Addadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Get Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getdigitalsignagecontentfolderdetails) | `zoom_rooms:read:digital_signage_library_contents:admin` | +| [Get Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Getdigitalsignagecontentfolderdetails) | `zoom_rooms:read:digital_signage_library_contents:master` | +| [Get Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetDigitalSignagelibraryplaylistcontentitems) | `zoom_rooms:read:digital_signage_library_playlists:admin` | +| [Get Digital Signage library playlist content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/GetDigitalSignagelibraryplaylistcontentitems) | `zoom_rooms:read:digital_signage_library_playlists:master` | +| [Get Digital Signage content item](/docs/api/rest/reference/zoom-rooms/methods/#operation/Getdigitalsignagecontentitem) | `zoom_rooms:read:digital_signage_library_contents:admin` | +| [Get Digital Signage content item](/docs/api/rest/reference/zoom-rooms/ma/#operation/Getdigitalsignagecontentitem) | `zoom_rooms:read:digital_signage_library_contents:master` | +| [Get Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetZoomRoomsBackgroundImageLibraryContent) | `zoom_rooms:read:background_library_content:admin` | +| [List Zoom Rooms background image library contents](/docs/api/rest/reference/zoom-rooms/methods/#operation/ListZoomRoomsbackgroundimagelibrarycontents) | `zoom_rooms:read:background_library_content:admin` | +| [Get Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/GetDigitalSignagelibraryplaylist) | `zoom_rooms:read:digital_signage_library_playlists:admin` | +| [Get Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/GetDigitalSignagelibraryplaylist) | `zoom_rooms:read:digital_signage_library_playlists:master` | +| [Delete a Digital Signage content item](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadigitalsignagecontentitem) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Delete a Digital Signage content item](/docs/api/rest/reference/zoom-rooms/ma/#operation/Deleteadigitalsignagecontentitem) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Add a digital signage image or video](/docs/api/rest/reference/zoom-rooms/methods/#operation/Adddigitalsignageimageorvideo) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Add a digital signage image or video](/docs/api/rest/reference/zoom-rooms/ma/#operation/Adddigitalsignageimageorvideo) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Delete a Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/methods/#operation/Deleteadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Delete a Digital Signage content folder](/docs/api/rest/reference/zoom-rooms/ma/#operation/Deleteadigitalsignagecontentfolder) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Update Zoom Rooms background image library content](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateZoomRoomsBackgroundImageLibraryContent) | `zoom_rooms:update:background_library_content:admin` | +| [List Digital Signage content items](/docs/api/rest/reference/zoom-rooms/methods/#operation/GETListdigitalsignagecontentitems) | `zoom_rooms:read:digital_signage_library_contents:admin` | +| [List Digital Signage content items](/docs/api/rest/reference/zoom-rooms/ma/#operation/GETListdigitalsignagecontentitems) | `zoom_rooms:read:digital_signage_library_contents:master` | +| [Update a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/methods/#operation/UpdateaDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:admin` | +| [Update a Digital Signage library playlist](/docs/api/rest/reference/zoom-rooms/ma/#operation/UpdateaDigitalSignagelibraryplaylist) | `zoom_rooms:write:digital_signage_library_playlists:master` | +| [Update a Digital Signage image or video file](/docs/api/rest/reference/zoom-rooms/methods/#operation/Updateadigitalsignageimageorvideofile) | `zoom_rooms:write:digital_signage_library_contents:admin` | +| [Update a Digital Signage image or video file](/docs/api/rest/reference/zoom-rooms/ma/#operation/Updateadigitalsignageimageorvideofile) | `zoom_rooms:write:digital_signage_library_contents:master` | +| [Delete Zoom Rooms Background Image Library Content](/docs/api/rest/reference/zoom-rooms/methods/#operation/DeleteZoomRoomsBackgroundImageLibraryContent) | `zoom_rooms:delete:background_library_content:admin` | + +### Zoom Rooms Devices + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Change Zoom Rooms app version](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeZoomRoomsAppVersion) | `zoom_rooms:update:device_app_version:admin` | +| [Change Zoom Rooms app version](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeZoomRoomsAppVersion) | `zoom_rooms:update:device_app_version:master` | +| [Delete a Zoom Room user device](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteDevice) | `zoom_rooms:delete:device:admin` | + +### Zoom Rooms Location + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Add a location](/docs/api/rest/reference/zoom-rooms/methods/#operation/addAZRLocation) | `zoom_rooms:write:location:admin` | +| [Add a location](/docs/api/rest/reference/zoom-rooms/ma/#operation/addAZRLocation) | `zoom_rooms:write:location:master` | +| [Get Zoom Room location structure](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationStructure) | `zoom_rooms:read:location_hierarchy:admin` | +| [Get Zoom Room location structure](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationStructure) | `zoom_rooms:read:location_hierarchy:master` | +| [Update Zoom Rooms location structure](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZoomRoomsLocationStructure) | `zoom_rooms:update:location_hierarchy:admin` | +| [Update Zoom Rooms location structure](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZoomRoomsLocationStructure) | `zoom_rooms:update:location_hierarchy:master` | +| [Delete a location](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteAZRLocation) | `zoom_rooms:write:location:admin` | +| [Delete a location](/docs/api/rest/reference/zoom-rooms/ma/#operation/deleteAZRLocation) | `zoom_rooms:write:location:master` | +| [Change the assigned parent location](/docs/api/rest/reference/zoom-rooms/methods/#operation/changeParentLocation) | `zoom_rooms:update:location:admin` | +| [Change the assigned parent location](/docs/api/rest/reference/zoom-rooms/ma/#operation/changeParentLocation) | `zoom_rooms:update:location:master` | +| [List Zoom Room locations](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZRLocations) | `zoom_rooms:read:list_locations:admin` | +| [List Zoom Room locations](/docs/api/rest/reference/zoom-rooms/ma/#operation/listZRLocations) | `zoom_rooms:read:list_locations:master` | +| [Update location settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRLocationSettings) | `zoom_rooms:update:location_settings:admin` | +| [Update location settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRLocationSettings) | `zoom_rooms:update:location_settings:master` | +| [Get location settings](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationSettings) | `zoom_rooms:read:location_settings:admin` | +| [Get location settings](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationSettings) | `zoom_rooms:read:location_settings:master` | +| [Update Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/updateZRLocationProfile) | `zoom_rooms:update:location:admin` | +| [Update Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/updateZRLocationProfile) | `zoom_rooms:update:location:master` | +| [Get Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/methods/#operation/getZRLocationProfile) | `zoom_rooms:read:location:admin` | +| [Get Zoom Room location profile](/docs/api/rest/reference/zoom-rooms/ma/#operation/getZRLocationProfile) | `zoom_rooms:read:location:master` | + +### Zoom Rooms Tags + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Assign Tags to Zoom Rooms By Location ID](/docs/api/rest/reference/zoom-rooms/methods/#operation/AssignTagsToZoomRoomsByLocationID) | `zoom_rooms:update:room_tag:admin` | +| [List all Zoom Room Tags](/docs/api/rest/reference/zoom-rooms/methods/#operation/listZoomRoomTags) | `zoom_rooms:read:list_tags:admin` | +| [Assign Tags to a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/AssignTagsToAZoomRoom) | `zoom_rooms:update:room_tag:admin` | +| [Create a new Zoom Rooms Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/createZoomRoomTag) | `zoom_rooms:write:tag:admin` | +| [Delete Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/deleteZoomRoomTag) | `zoom_rooms:delete:tag:admin` | +| [Un-assign Tags from a Zoom Room](/docs/api/rest/reference/zoom-rooms/methods/#operation/unassignZoomRoomTag) | `zoom_rooms:delete:room_tag:admin` | +| [Edit Tag](/docs/api/rest/reference/zoom-rooms/methods/#operation/editZoomRoomTag) | `zoom_rooms:update:tag:admin` | + +## Zoom Scheduler + + +### Routing Forms + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [get routing response](/docs/api/rest/reference/phone/methods/#operation/Getroutingresponse) | `scheduler:read:routing`, `scheduler:read:routing:admin` | + +### analytics + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Report analytics](/docs/api/rest/reference/phone/methods/#operation/report_analytics) | `scheduler:read:analytics`, `scheduler:read:analytics:admin` | + +### availability + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get availability](/docs/api/rest/reference/phone/methods/#operation/get_availability) | `scheduler:read:availability`, `scheduler:read:availability:admin` | +| [Patch availability](/docs/api/rest/reference/phone/methods/#operation/patch_availability) | `scheduler:update:availability`, `scheduler:update:availability:admin` | +| [Insert availability](/docs/api/rest/reference/phone/methods/#operation/insert_availability) | `scheduler:write:availability`, `scheduler:write:availability:admin` | +| [Delete availability](/docs/api/rest/reference/phone/methods/#operation/delete_availability) | `scheduler:delete:availability`, `scheduler:delete:availability:admin` | +| [List availability](/docs/api/rest/reference/phone/methods/#operation/list_availability) | `scheduler:read:list_availability`, `scheduler:read:list_availability:admin` | + +### scheduled events + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Patch scheduled events](/docs/api/rest/reference/phone/methods/#operation/patch_scheduled_events) | `scheduler:update:scheduled_event`, `scheduler:update:scheduled_event:admin` | +| [Get scheduled events](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_events) | `scheduler:read:scheduled_event`, `scheduler:read:scheduled_event:admin` | +| [Get scheduled event attendee](/docs/api/rest/reference/phone/methods/#operation/get_scheduled_event_attendee) | `scheduler:read:scheduled_event_attendee`, `scheduler:read:scheduled_event_attendee:admin` | +| [List scheduled events](/docs/api/rest/reference/phone/methods/#operation/list_scheduled_events) | `scheduler:read:list_scheduled_events`, `scheduler:read:list_scheduled_events:admin` | +| [Delete scheduled events](/docs/api/rest/reference/phone/methods/#operation/delete_scheduled_events) | `scheduler:delete:scheduled_event`, `scheduler:delete:scheduled_event:admin` | + +### schedules + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List schedules](/docs/api/rest/reference/phone/methods/#operation/list_schedules) | `scheduler:read:list_schedule`, `scheduler:read:list_schedule:admin` | +| [Patch schedules](/docs/api/rest/reference/phone/methods/#operation/patch_schedule) | `scheduler:update:patch_schedule`, `scheduler:update:patch_schedule:admin` | +| [Insert schedules](/docs/api/rest/reference/phone/methods/#operation/insert_schedule) | `scheduler:write:insert_schedule`, `scheduler:write:insert_schedule:admin` | +| [Get schedules](/docs/api/rest/reference/phone/methods/#operation/get_schedule) | `scheduler:read:get_schedule`, `scheduler:read:get_schedule:admin` | +| [Delete schedules](/docs/api/rest/reference/phone/methods/#operation/delete_schedules) | `scheduler:delete:delete_schedule`, `scheduler:delete:delete_schedule:admin` | + +### scheduling links + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Single use link](/docs/api/rest/reference/phone/methods/#operation/single_use_link) | `scheduler:write:single_use_link`, `scheduler:write:single_use_link:admin` | + +### shares + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create shares](/docs/api/rest/reference/phone/methods/#operation/create_shares) | `scheduler:write:share`, `scheduler:write:share:admin` | + +### team + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List team](/docs/api/rest/reference/phone/methods/#operation/Listteam) | `scheduler:read:get_schedule`, `scheduler:read:get_schedule:admin` | + +### users + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get user](/docs/api/rest/reference/phone/methods/#operation/get_user) | `scheduler:read:user`, `scheduler:read:user:admin` | + +## Zoom User + + +### Divisions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Create a division](/docs/api/rest/reference/user/methods/#operation/Createadivision) | `division:write:division:admin` | +| [Get a division](/docs/api/rest/reference/user/methods/#operation/Getdivision) | `division:read:division:admin` | +| [List divisions](/docs/api/rest/reference/user/methods/#operation/listDivisions) | `division:read:list_divisions:admin` | +| [List division members](/docs/api/rest/reference/user/methods/#operation/listDivisionMembers) | `division:read:member:admin` | +| [Delete a division](/docs/api/rest/reference/user/methods/#operation/Deletedivision) | `division:delete:division:admin` | +| [Assign a division](/docs/api/rest/reference/user/methods/#operation/assigndivisionMember) | `division:write:member:admin` | +| [Update a division](/docs/api/rest/reference/user/methods/#operation/Updateadivision) | `division:update:division:admin` | + +### Groups + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete Virtual Background files](/docs/api/rest/reference/user/methods/#operation/delGroupVB) | `group:delete:virtual_background_files:admin` | +| [Delete Virtual Background files](/docs/api/rest/reference/user/ma/#operation/delGroupVB) | `group:delete:virtual_background_files:master` | +| [Upload Virtual Background files](/docs/api/rest/reference/user/methods/#operation/uploadGroupVB) | `group:write:virtual_background_files:admin` | +| [Upload Virtual Background files](/docs/api/rest/reference/user/ma/#operation/uploadGroupVB) | `group:write:virtual_background_files:master` | +| [List group channels](/docs/api/rest/reference/user/methods/#operation/groupChannels) | `group:read:list_channels:admin` | +| [List group channels](/docs/api/rest/reference/user/ma/#operation/groupChannels) | `group:read:list_channels:master` | +| [Add group members](/docs/api/rest/reference/user/methods/#operation/groupMembersCreate) | `group:write:member:admin` | +| [Add group members](/docs/api/rest/reference/user/ma/#operation/groupMembersCreate) | `group:write:member:master` | +| [Delete a group admin](/docs/api/rest/reference/user/methods/#operation/groupAdminsDelete) | `group:delete:administrator:admin` | +| [Delete a group admin](/docs/api/rest/reference/user/ma/#operation/groupAdminsDelete) | `group:delete:administrator:master` | +| [Update a group](/docs/api/rest/reference/user/methods/#operation/groupUpdate) | `group:update:group:admin` | +| [Update a group](/docs/api/rest/reference/user/ma/#operation/groupUpdate) | `group:update:group:master` | +| [Update locked settings](/docs/api/rest/reference/user/methods/#operation/groupLockedSettings) | `group:update:lock_settings:admin` | +| [Update locked settings](/docs/api/rest/reference/user/ma/#operation/groupLockedSettings) | `group:update:lock_settings:master` | +| [Create a group](/docs/api/rest/reference/user/methods/#operation/groupCreate) | `group:write:group:admin` | +| [Create a group](/docs/api/rest/reference/user/ma/#operation/groupCreate) | `group:write:group:master` | +| [Get locked settings](/docs/api/rest/reference/user/methods/#operation/getGroupLockSettings) | `group:read:lock_settings:admin` | +| [Get locked settings](/docs/api/rest/reference/user/ma/#operation/getGroupLockSettings) | `group:read:lock_settings:master` | +| [Update a group's settings](/docs/api/rest/reference/user/methods/#operation/updateGroupSettings) | `group:update:settings:admin` | +| [Update a group's settings](/docs/api/rest/reference/user/ma/#operation/updateGroupSettings) | `group:update:settings:master` | +| [Update a group's webinar registration settings](/docs/api/rest/reference/user/methods/#operation/groupSettingsRegistrationUpdate) | `group:update:registration_settings:admin` | +| [Update a group's webinar registration settings](/docs/api/rest/reference/user/ma/#operation/groupSettingsRegistrationUpdate) | `group:update:registration_settings:master` | +| [List group admins](/docs/api/rest/reference/user/methods/#operation/groupAdmins) | `group:read:administrator:admin` | +| [List group admins](/docs/api/rest/reference/user/ma/#operation/groupAdmins) | `group:read:administrator:master` | +| [Delete a group member](/docs/api/rest/reference/user/methods/#operation/groupMembersDelete) | `group:delete:member:admin` | +| [Delete a group member](/docs/api/rest/reference/user/ma/#operation/groupMembersDelete) | `group:delete:member:master` | +| [List groups](/docs/api/rest/reference/user/methods/#operation/groups) | `group:read:list_groups:admin` | +| [List groups](/docs/api/rest/reference/user/ma/#operation/groups) | `group:read:list_groups:master` | +| [Get a group](/docs/api/rest/reference/user/methods/#operation/group) | `group:read:group:admin` | +| [Get a group](/docs/api/rest/reference/user/ma/#operation/group) | `group:read:group:master` | +| [Get a group's settings](/docs/api/rest/reference/user/methods/#operation/getGroupSettings) | `group:read:settings:admin` | +| [Get a group's settings](/docs/api/rest/reference/user/ma/#operation/getGroupSettings) | `group:read:settings:master` | +| [Add group admins](/docs/api/rest/reference/user/methods/#operation/groupAdminsCreate) | `group:write:administrator:admin` | +| [List group members](/docs/api/rest/reference/user/methods/#operation/groupMembers) | `group:read:list_members:admin` | +| [List group members](/docs/api/rest/reference/user/ma/#operation/groupMembers) | `group:read:list_members:master` | +| [Update a group member](/docs/api/rest/reference/user/methods/#operation/updateAGroupMember) | `group:update:member:admin` | +| [Update a group member](/docs/api/rest/reference/user/ma/#operation/updateAGroupMember) | `group:update:member:master` | +| [Delete a group](/docs/api/rest/reference/user/methods/#operation/groupDelete) | `group:delete:group:admin` | +| [Delete a group](/docs/api/rest/reference/user/ma/#operation/groupDelete) | `group:delete:group:master` | +| [Get a group's webinar registration settings](/docs/api/rest/reference/user/methods/#operation/groupSettingsRegistration) | `group:read:registration_settings:admin` | +| [Get a group's webinar registration settings](/docs/api/rest/reference/user/ma/#operation/groupSettingsRegistration) | `group:read:registration_settings:master` | + +### Users + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List a user's collaboration devices](/docs/api/rest/reference/user/methods/#operation/listCollaborationDevices) | `user:read:list_collaboration_devices`, `user:read:list_collaboration_devices:admin` | +| [Get meeting summary templates](/docs/api/rest/reference/user/methods/#operation/Getmeetingsummarytemplates) | `user:read:settings`, `user:read:settings:admin` | +| [Get user permissions](/docs/api/rest/reference/user/methods/#operation/userPermission) | `user:read:list_permissions`, `user:read:list_permissions:admin` | +| [Get user permissions](/docs/api/rest/reference/user/ma/#operation/userPermission) | `user:read:list_permissions:master` | +| [Delete a scheduler](/docs/api/rest/reference/user/methods/#operation/userSchedulerDelete) | `user:delete:scheduler`, `user:delete:scheduler:admin` | +| [Delete a scheduler](/docs/api/rest/reference/user/ma/#operation/userSchedulerDelete) | `user:delete:scheduler:master` | +| [Get user settings](/docs/api/rest/reference/user/methods/#operation/userSettings) | `user:read:settings`, `user:read:settings:admin` | +| [Get user settings](/docs/api/rest/reference/user/ma/#operation/userSettings) | `user:read:settings:master` | +| [Add assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantCreate) | `user:write:assistant`, `user:write:assistant:admin` | +| [Add assistants](/docs/api/rest/reference/user/ma/#operation/userAssistantCreate) | `user:write:assistant:master` | +| [Update a user's presence status](/docs/api/rest/reference/user/methods/#operation/updatePresenceStatus) | `user:update:presence_status`, `user:update:presence_status:admin` | +| [Get a user](/docs/api/rest/reference/user/methods/#operation/user) | `user:read:user`, `user:read:user:admin` | +| [Get a user](/docs/api/rest/reference/user/ma/#operation/user) | `user:read:user:master` | +| [Upload a user's profile picture](/docs/api/rest/reference/user/methods/#operation/userPicture) | `user:write:profile_picture`, `user:write:profile_picture:admin` | +| [Upload a user's profile picture](/docs/api/rest/reference/user/ma/#operation/userPicture) | `user:write:profile_picture:master` | +| [Update user status](/docs/api/rest/reference/user/methods/#operation/userStatus) | `user:update:status`, `user:update:status:admin` | +| [Update user status](/docs/api/rest/reference/user/ma/#operation/userStatus) | `user:update:status:master` | +| [Get user summary](/docs/api/rest/reference/user/methods/#operation/userSummary) | `user:read:summary:admin` | +| [Get user summary](/docs/api/rest/reference/user/ma/#operation/userSummary) | `user:read:summary:master` | +| [Delete user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulersDelete) | `user:delete:scheduler`, `user:delete:scheduler:admin` | +| [Delete user schedulers](/docs/api/rest/reference/user/ma/#operation/userSchedulersDelete) | `user:delete:scheduler:master` | +| [Get a user presence status](/docs/api/rest/reference/user/methods/#operation/getUserPresenceStatus) | `user:read:presence_status`, `user:read:presence_status:admin` | +| [List user schedulers](/docs/api/rest/reference/user/methods/#operation/userSchedulers) | `user:read:list_schedulers`, `user:read:list_schedulers:admin` | +| [List user schedulers](/docs/api/rest/reference/user/ma/#operation/userSchedulers) | `user:read:list_schedulers:master` | +| [Delete a user assistant](/docs/api/rest/reference/user/methods/#operation/userAssistantDelete) | `user:delete:assistant`, `user:delete:assistant:admin` | +| [Delete a user assistant](/docs/api/rest/reference/user/ma/#operation/userAssistantDelete) | `user:delete:assistant:master` | +| [Delete user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistantsDelete) | `user:delete:assistant`, `user:delete:assistant:admin` | +| [Delete user assistants](/docs/api/rest/reference/user/ma/#operation/userAssistantsDelete) | `user:delete:assistant:master` | +| [List user assistants](/docs/api/rest/reference/user/methods/#operation/userAssistants) | `user:read:list_assistants`, `user:read:list_assistants:admin` | +| [List user assistants](/docs/api/rest/reference/user/ma/#operation/userAssistants) | `user:read:list_assistants:master` | +| [Delete a user](/docs/api/rest/reference/user/methods/#operation/userDelete) | `user:delete:user`, `user:delete:user:admin` | +| [Delete a user](/docs/api/rest/reference/user/ma/#operation/userDelete) | `user:delete:user:master` | +| [Get the user's ZAK](/docs/api/rest/reference/user/methods/#operation/userZak) | `user:read:zak`, `user:read:zak:admin` | +| [Update a user's email](/docs/api/rest/reference/user/methods/#operation/userEmailUpdate) | `user:update:email`, `user:update:email:admin` | +| [Update a user's email](/docs/api/rest/reference/user/ma/#operation/userEmailUpdate) | `user:update:email:master` | +| [List users](/docs/api/rest/reference/user/methods/#operation/users) | `user:read:list_users:admin` | +| [List users](/docs/api/rest/reference/user/ma/#operation/users) | `user:read:list_users:master` | +| [Revoke a user's SSO token](/docs/api/rest/reference/user/methods/#operation/userSSOTokenDelete) | `user:delete:token`, `user:delete:token:admin` | +| [Revoke a user's SSO token](/docs/api/rest/reference/user/ma/#operation/userSSOTokenDelete) | `user:delete:token:master` | +| [Update a user](/docs/api/rest/reference/user/methods/#operation/userUpdate) | `user:update:user`, `user:update:user:admin` | +| [Update a user](/docs/api/rest/reference/user/ma/#operation/userUpdate) | `user:update:user:master` | +| [Delete Virtual Background files](/docs/api/rest/reference/user/methods/#operation/delUserVB) | `user:delete:virtual_background_files`, `user:delete:virtual_background_files:admin` | +| [Delete Virtual Background files](/docs/api/rest/reference/user/ma/#operation/delUserVB) | `user:delete:virtual_background_files:master` | +| [Upload Virtual Background files](/docs/api/rest/reference/user/methods/#operation/uploadVBuser) | `user:write:virtual_background_files`, `user:write:virtual_background_files:admin` | +| [Upload Virtual Background files](/docs/api/rest/reference/user/ma/#operation/uploadVBuser) | `user:write:virtual_background_files:master` | +| [Get a user's token](/docs/api/rest/reference/user/methods/#operation/userToken) | `user:read:token`, `user:read:token:admin` | +| [Get a user's token](/docs/api/rest/reference/user/ma/#operation/userToken) | `user:read:token:master` | +| [Bulk update features for users](/docs/api/rest/reference/user/methods/#operation/bulkUpdateFeature) | `user:write:feature:admin` | +| [Bulk update features for users](/docs/api/rest/reference/user/ma/#operation/bulkUpdateFeature) | `user:write:feature:master` | +| [Update user settings](/docs/api/rest/reference/user/methods/#operation/userSettingsUpdate) | `user:update:settings`, `user:update:settings:admin` | +| [Update user settings](/docs/api/rest/reference/user/ma/#operation/userSettingsUpdate) | `user:update:settings:master` | +| [Create users](/docs/api/rest/reference/user/methods/#operation/userCreate) | `user:write:user:admin` | +| [Create users](/docs/api/rest/reference/user/ma/#operation/userCreate) | `user:write:user:master` | +| [Get collaboration device detail](/docs/api/rest/reference/user/methods/#operation/getCollaborationDevice) | `user:read:collaboration_device`, `user:read:collaboration_device:admin` | +| [Check a user's PM room](/docs/api/rest/reference/user/methods/#operation/userVanityName) | `user:read:pm_room`, `user:read:pm_room:admin` | +| [Update a user's password](/docs/api/rest/reference/user/methods/#operation/userPassword) | `user:update:password`, `user:update:password:admin` | +| [Update a user's password](/docs/api/rest/reference/user/ma/#operation/userPassword) | `user:update:password:master` | +| [Check a user email](/docs/api/rest/reference/user/methods/#operation/userEmail) | `user:read:email`, `user:read:email:admin` | + +## Zoom Video Management + + +### Channels + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Update channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdateVideoChannel) | `video_mgmt:update:channel`, `video_mgmt:update:channel:admin` | +| [List channels](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listVideoChannels) | `video_mgmt:read:list_channels`, `video_mgmt:read:list_channels:admin` | +| [Get channel details](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/getChannelDetail) | `video_mgmt:read:channel`, `video_mgmt:read:channel:admin` | +| [Channel actions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/channelActions) | `video_mgmt:update:channel`, `video_mgmt:update:channel:admin` | +| [Create a channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannel) | `video_mgmt:write:channel`, `video_mgmt:write:channel:admin` | +| [Delete channel](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannel) | `video_mgmt:delete:channel`, `video_mgmt:delete:channel:admin` | + +### Permissions + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/listChannelPermissions) | `video_mgmt:read:list_permissions`, `video_mgmt:read:list_permissions:admin` | +| [Update channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/updateChannelPermissions) | `video_mgmt:update:permissions`, `video_mgmt:update:permissions:admin` | +| [Create channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createChannelPermissions) | `video_mgmt:write:permissions`, `video_mgmt:write:permissions:admin` | +| [Delete channel permissions](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPermissions) | `video_mgmt:delete:permissions`, `video_mgmt:delete:permissions:admin` | + +### Playlists + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Delete playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylist) | `video_mgmt:delete:playlist`, `video_mgmt:delete:playlist:admin` | +| [Add channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelPlaylists) | `video_mgmt:write:channel_playlists`, `video_mgmt:write:channel_playlists:admin` | +| [List playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylists) | `video_mgmt:read:list_playlists`, `video_mgmt:read:list_playlists:admin` | +| [List channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelPlaylists) | `video_mgmt:read:list_channel_playlists`, `video_mgmt:read:list_channel_playlists:admin` | +| [Create a playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/createPlaylist) | `video_mgmt:write:playlist`, `video_mgmt:write:playlist:admin` | +| [Delete channel playlists](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelPlaylists) | `video_mgmt:delete:channel_playlists`, `video_mgmt:delete:channel_playlists:admin` | +| [Update playlist](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/UpdatePlaylist) | `video_mgmt:update:playlist`, `video_mgmt:update:playlist:admin` | + +### Videos + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [List playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListPlaylistVideos) | `video_mgmt:read:list_playlist_videos`, `video_mgmt:read:list_playlist_videos:admin` | +| [Delete playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeletePlaylistVideos) | `video_mgmt:delete:playlist_videos`, `video_mgmt:delete:playlist_videos:admin` | +| [Add playlist videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddPlaylistVideos) | `video_mgmt:write:playlist_videos`, `video_mgmt:write:playlist_videos:admin` | +| [List all videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListAllVideos) | `video_mgmt:read:list_videos`, `video_mgmt:read:list_videos:admin` | +| [Add channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/AddChannelVideos) | `video_mgmt:write:channel_videos`, `video_mgmt:write:channel_videos:admin` | +| [List channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/ListChannelVideos) | `video_mgmt:read:list_channel_videos`, `video_mgmt:read:list_channel_videos:admin` | +| [Delete channel videos](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/DeleteChannelVideos) | `video_mgmt:delete:channel_videos`, `video_mgmt:delete:channel_videos:admin` | + +### files + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Upload file for video management](/docs/api/rest/reference/https:/developers.zoom.us/docs/api/methods/#operation/uploadVODtFile) | `video_mgmt:write:file`, `video_mgmt:write:file:admin` | + +## Zoom Virtual Agent + + +### Knowledge Management + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get articles](/docs/api/rest/reference/virtual-agent/methods/#operation/GetArticles) | `zva:read:list_km_articles`, `zva:read:list_km_articles:admin` | +| [Get sync](/docs/api/rest/reference/virtual-agent/methods/#operation/GetSync) | `zva:read:km_kb:admin` | +| [Get article](/docs/api/rest/reference/virtual-agent/methods/#operation/GetArticle) | `zva:read:km_article`, `zva:read:km_article:admin` | +| [Delete article](/docs/api/rest/reference/virtual-agent/methods/#operation/DeleteArticle) | `zva:delete:km_article`, `zva:delete:km_article:admin` | +| [Create sync request](/docs/api/rest/reference/virtual-agent/methods/#operation/CreateSyncRequest) | `zva:update:km_kb:admin` | +| [Update article](/docs/api/rest/reference/virtual-agent/methods/#operation/UpdateArticle) | `zva:update:km_article`, `zva:update:km_article:admin` | +| [Create article](/docs/api/rest/reference/virtual-agent/methods/#operation/CreateArticle) | `zva:write:km_article`, `zva:write:km_article:admin` | + +### Report + +| API Endpoint | Granular Scopes | +|--------------|-----------------| +| [Get ZVA engagements](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAEngagements) | `zva:read:list_engagements:admin` | +| [Get ZVA query details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAQueryDetails) | `zva:read:list_queries:admin` | +| [Get ZVA Surveys](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVASurveys) | `zva:read:list_surveys:admin` | +| [Get ZVA variable details](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVAengagementvariabledetails) | `zva:read:list_variables:admin` | +| [Get ZVA transcripts](/docs/api/rest/reference/virtual-agent/methods/#operation/GetZVATranscripts) | `zva:read:list_transcripts:admin` | \ No newline at end of file diff --git a/partner-built/zoom-plugin/skills/oauth/references/oauth-errors.md b/partner-built/zoom-plugin/skills/oauth/references/oauth-errors.md new file mode 100644 index 00000000..17e82d11 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/references/oauth-errors.md @@ -0,0 +1,41 @@ +# OAuth Error Messages + +> Source: https://developers.zoom.us/docs/integrations/oauth/ + +This table lists OAuth error messages, possible causes, and recommended mitigations. + +| Error Code | Error Message | Description | Guidance | +|------------|---------------|-------------|----------| +| 4700 | *(empty)* | The cause can vary depending on the API. | Use the tracking ID to find additional information in the logs and contact Zoom for further assistance. | +| 4700 | Token cannot be empty. | The token is missing. | Verify that the token is present in the header and that its value is correct. | +| 4700 | Exception message | This is a catch all for unexpected errors. | Report the error code to Zoom for further assistance. | +| 4702, 4704 | Invalid client. / Invalid client secret. | Client ID does not match authenticated client. The client ID or client secret isn't entered correctly, or the related app doesn't exist. | Verify that the client ID and client secret are entered in the header correctly. If they are correct then contact Zoom for further assistance. | +| 4705 | Grant type is not supported from token endpoint. | The grant type is not supported by the token endpoint. | Use a valid grant type against `https://zoom.us/oauth/token` (for example `authorization_code`, `refresh_token`, `account_credentials`, `client_credentials`, `urn:ietf:params:oauth:grant-type:device_code`). | +| 4706 | Client ID or client secret is missing. | The client ID and client secret are missing either in the header or in the request parameter. | Verify that the client ID and client secret are entered correctly in the header or request parameter. | +| 4706 | Missing grant type. | OAuth requires the grant type, and it is missing in the header. | Verify that the grant type is entered in the header. | +| 4709 | Redirect URI mismatch. | The redirect_uri is missing or the value is null or is incorrect. | Verify that the redirect_uri is entered correctly. | +| 4711 | Refresh token invalid. | The token scopes do not match with the client scopes. | Verify that there isn't a mismatch between the token's scopes and the client's scopes. | +| 4717 | The app has been disabled | The app has been disabled. | Contact Zoom support to enable the app. | +| 4724 | Exception error message. | An invalid JWT token is passed in the header. | Verify that the JWT token is correctly signed and that the token passed in the header is valid. | +| 4732 | Creating authorization code error. | The lookup service may be down. ELK logs usually throw a `/lookup/v1/indexes POST 5005` Internal Server error. | Contact your DNS lookup service provider to verify the server status, or contact Zoom for further support. | +| 4733 | Code is expired | Authorization codes have an expiration time of 5 minutes. | Regenerate the authorization code. | +| 4734 | Invalid authorization code. | The authorization code is invalid. | Regenerate the authorization code. | +| 4735 | The owner of the token does not exist. | The user ID of the token doesn't exist. This might happen if the refresh token was issued for a user who has since been removed from an account. The user ID is stored in the `uid` field of a token. | Verify the `uid` for the token is valid and entered correctly. | +| 4737 | Can not find the authentication for the access token. | The refresh token isn't found in the DynamoDB table. | Contact Zoom and request to reauthorize the app. | +| 4738 | The token is disabled by admin. | An admin turned off pre-approval for the related app for users under an account. | Contact Zoom for further support. | +| 4740 | The token ID is out of the token tolerance range. | The maximum number of times a refresh token is allowed to be used has been surpassed. Tolerance errors happen with version 7 tokens. Version 8 and later tokens do not use the tolerance mechanism. | Contact Zoom for assistance with reconfiguring the tolerance range. | +| 4741 | The token has been revoked. | This happens when you perform multiple authorizations. With multiple authorizations, the last token issued is considered valid, and the previous ones are invalidated. | Make sure you are using the latest and valid authorization token. | + +## Common Issues Quick Reference + +| Symptom | Check | +|---------|-------| +| Empty error (4700) | Check tracking ID in logs | +| Invalid client (4702/4704) | Verify Client ID and Client Secret | +| Grant type error (4705) | Use: `refresh_token`, `authorization_code`, `device_auth`, `account_credentials` | +| Missing credentials (4706) | Ensure Client ID/Secret in header or request params | +| Redirect mismatch (4709) | Verify redirect_uri matches app configuration | +| Token scope mismatch (4711) | Compare token scopes vs client scopes | +| Code expired (4733) | Authorization codes expire in 5 minutes | +| Invalid code (4734) | Regenerate authorization code | +| Token revoked (4741) | Use the most recent token from latest authorization | diff --git a/partner-built/zoom-plugin/skills/oauth/troubleshooting/common-errors.md b/partner-built/zoom-plugin/skills/oauth/troubleshooting/common-errors.md new file mode 100644 index 00000000..376e6f1d --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/troubleshooting/common-errors.md @@ -0,0 +1,13 @@ +# Common Errors + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. + +## High-Frequency Endpoint Mistake + +- Use `https://zoom.us/oauth/authorize` for user consent. +- Use `https://zoom.us/oauth/token` for token exchange. +- If token calls return HTML or 404, check that you are not calling `/oauth/token`. diff --git a/partner-built/zoom-plugin/skills/oauth/troubleshooting/redirect-uri-issues.md b/partner-built/zoom-plugin/skills/oauth/troubleshooting/redirect-uri-issues.md new file mode 100644 index 00000000..6a21b3ba --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/troubleshooting/redirect-uri-issues.md @@ -0,0 +1,7 @@ +# Redirect Uri Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/partner-built/zoom-plugin/skills/oauth/troubleshooting/scope-issues.md b/partner-built/zoom-plugin/skills/oauth/troubleshooting/scope-issues.md new file mode 100644 index 00000000..31ec61f5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/troubleshooting/scope-issues.md @@ -0,0 +1,7 @@ +# Scope Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/partner-built/zoom-plugin/skills/oauth/troubleshooting/token-issues.md b/partner-built/zoom-plugin/skills/oauth/troubleshooting/token-issues.md new file mode 100644 index 00000000..36a1126b --- /dev/null +++ b/partner-built/zoom-plugin/skills/oauth/troubleshooting/token-issues.md @@ -0,0 +1,7 @@ +# Token Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/partner-built/zoom-plugin/skills/phone/RUNBOOK.md b/partner-built/zoom-plugin/skills/phone/RUNBOOK.md new file mode 100644 index 00000000..6dde6f8e --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/RUNBOOK.md @@ -0,0 +1,47 @@ +# Zoom Phone 5-Minute Preflight Runbook + +Use this before deep debugging. + +## 1) Confirm Product Prerequisites + +- Zoom Phone licenses assigned. +- Admin access available for Phone settings. +- If SMS is required, 10DLC/SMS setup is complete. + +## 2) Confirm App and OAuth + +- App type: General OAuth app for user/admin flows. +- Redirect URI and allow list are exact and current. +- Required Phone scopes are added. +- App is installed/re-authorized after scope changes. + +## 3) Confirm Integration Surface + +- Smart Embed: iframe/script loaded and approved domain configured. +- API/Webhook: access token valid and webhook endpoint reachable. +- URI launch: endpoint uses supported scheme and client is signed in. + +## 4) Confirm Event/Data Correlation + +- Persist `call_id` for real-time events. +- Persist `call_history_uuid` and `call_element_id` for post-call lookup. +- Keep idempotency logic for duplicate event deliveries. + +## 5) Confirm Migration Posture + +- Do not build new features on legacy v1 call logs. +- Webhook consumers are ready for `call_element` event names/fields. +- Field-mapping adapter exists for old/new payload shapes. + +## 6) Confirm Security Controls + +- Smart Embed `postMessage` enforces trusted origin. +- Webhook signatures validated with secret token. +- OAuth secrets are server-side only. + +## 7) Fast Decision Tree + +- Smart Embed iframe visible but no events -> missing init sequence or bad origin filtering. +- OAuth works but API fails with 401/403 -> scope mismatch or stale authorization. +- Data pipeline breaks after endpoint/event upgrade -> missing v2/v3 field mapping. +- URI click does nothing -> unsupported platform/client state, or wrong scheme. diff --git a/partner-built/zoom-plugin/skills/phone/SKILL.md b/partner-built/zoom-plugin/skills/phone/SKILL.md new file mode 100644 index 00000000..004e58c9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/SKILL.md @@ -0,0 +1,85 @@ +--- +name: build-zoom-phone-integration +description: "Reference skill for Zoom Phone. Use after routing to a phone workflow when implementing OAuth, Phone APIs, webhooks, Smart Embed events, URI schemes, CRM or CTI dialers, or call handling automation." +triggers: + - "zoom phone" + - "phone smart embed" + - "zoom phone api" + - "zoom phone webhook" + - "call history" + - "call handling" + - "zoomphonecall" + - "zoomphonesms" + - "phone crm integration" + - "call element" +--- + +# /build-zoom-phone-integration + +Background reference for Zoom Phone integrations across API, webhook, Smart Embed, and URI-launch workflows. + +Implementation guidance for Zoom Phone integrations across API, webhook/event, Smart Embed, and URI-launch workflows. + +Official docs: +- https://developers.zoom.us/docs/phone/ +- CRM sample reference: https://github.com/zoom/CRM-Sample + +## Routing Guardrail + +- If the user needs embedded softphone behavior in a web app, use Smart Embed ([examples/smart-embed-postmessage-bridge.md](examples/smart-embed-postmessage-bridge.md)). +- If the user needs call records, analytics, or automation, use Phone REST API and webhooks ([references/deprecations-and-migrations.md](references/deprecations-and-migrations.md)). +- If the user needs click-to-dial/SMS launch from external UI, use URI schemes (`zoomphonecall://`, `zoomphonesms://`). +- If the user mixes Zoom Phone and Contact Center, chain with [../contact-center/SKILL.md](../contact-center/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +3. [references/deprecations-and-migrations.md](references/deprecations-and-migrations.md) +4. [references/forum-top-questions.md](references/forum-top-questions.md) +5. [references/smart-embed-event-contract.md](references/smart-embed-event-contract.md) +6. [references/call-handling-patterns.md](references/call-handling-patterns.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/crm-sample-validation.md](references/crm-sample-validation.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) +10. [RUNBOOK.md](RUNBOOK.md) +11. [examples/smart-embed-postmessage-bridge.md](examples/smart-embed-postmessage-bridge.md) +12. [examples/phone-api-service-pattern.md](examples/phone-api-service-pattern.md) +13. [references/source-map.md](references/source-map.md) + +## Common Lifecycle Pattern + +1. Provision account prerequisites (Zoom Phone license, admin setup, SMS readiness). +2. Create OAuth app and scopes in Marketplace. +3. Choose integration surface: +- Smart Embed (iframe + postMessage) +- REST + webhooks +- URI launch (`callto`, `tel`, `zoomphonecall`, `zoomphonesms`) +4. Capture real-time events (Smart Embed events and/or webhooks). +5. Persist call identifiers and correlate records (`call_id`, `call_history_uuid`, `call_element_id`). +6. Apply migration-safe data mapping (v1 -> v2 -> v3) and handle renamed fields. +7. Harden security (origin validation, webhook signature validation, least-privilege scopes). + +## High-Level Scenarios + +- CRM softphone pane using Smart Embed + contact search/match callbacks. +- Click-to-call from account/contact table via `zp-make-call`. +- Call disposition workflow using `zp-save-log-event` and custom notes page. +- SMS engagement workflow with `zoomphonesms://` and `zp-sms-log-event`. +- Real-time operational board driven by `phone.*` webhook events. +- Call analytics migration from legacy call logs to call history/call elements. +- Admin automation for user/auto-receptionist/call-queue call-handling settings. + +See [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) for details. + +## Chaining + +- OAuth setup/token lifecycle: [../oauth/SKILL.md](../oauth/SKILL.md) +- Phone and account resources via REST: [../rest-api/SKILL.md](../rest-api/SKILL.md) +- Event delivery and signature validation: [../webhooks/SKILL.md](../webhooks/SKILL.md) +- Contact Center blended journey: [../contact-center/SKILL.md](../contact-center/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/phone/concepts/architecture-and-lifecycle.md b/partner-built/zoom-plugin/skills/phone/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..bd25fa0d --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/concepts/architecture-and-lifecycle.md @@ -0,0 +1,57 @@ +# Zoom Phone Architecture and Lifecycle + +## Architecture + +```text +User/Agent UI + | + | (A) Smart Embed postMessage events + v +Smart Embed Iframe (applications.zoom.us) + | + | event stream + call controls + v +CRM Web App (event bridge + UI state) + | + | OAuth token on server only + v +Backend API Layer + |\ + | \-- Zoom Phone REST APIs (call history, call handling, contacts) + | + \---- Webhook endpoint (phone.* events) +``` + +## Lifecycle Workflow + +1. Provision: +- Account has Zoom Phone and optional SMS enablement. + +2. Authorize: +- OAuth app installed and scoped for required Phone operations. + +3. Initialize UI: +- Load Smart Embed iframe/script. +- Wait for `onZoomPhoneIframeApiReady`. +- Send `zp-init-config` and register event handlers. + +4. Engage: +- Start calls/SMS via `zp-make-call` or `zp-input-sms`. +- Receive events (`zp-call-*`, `zp-sms-log-event`, optional AI/contact/notes events). + +5. Persist: +- Save event snapshots keyed by `callId`. +- Reconcile to call history/call element records after completion. + +6. Post-call: +- Save call notes/disposition and optional recording/voicemail links. + +7. Operate: +- Track deprecations and apply endpoint/event mapping updates. + +## Version Drift Strategy + +- Normalize inbound payloads in one adapter layer. +- Keep endpoint constants centralized by version target. +- Feature-flag optional payload fields. +- Keep webhook + Smart Embed event handlers tolerant to added fields and enum expansion. diff --git a/partner-built/zoom-plugin/skills/phone/examples/phone-api-service-pattern.md b/partner-built/zoom-plugin/skills/phone/examples/phone-api-service-pattern.md new file mode 100644 index 00000000..f3d2bf41 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/examples/phone-api-service-pattern.md @@ -0,0 +1,41 @@ +# Phone API Service Pattern (Migration-Safe) + +## Pattern goals + +- Isolate OAuth token usage to server code. +- Support current call history/call element model. +- Keep compatibility with old payload fields while migrating. + +## Service example + +```javascript +export async function getCallHistory(accessToken, from, to) { + const qs = new URLSearchParams({ from, to }).toString(); + const res = await fetch(`https://api.zoom.us/v2/phone/call_history?${qs}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) throw new Error(`call_history failed: ${res.status}`); + + const data = await res.json(); + + // Normalize v2/v3 style for downstream code. + return (data.call_history || data.call_logs || []).map((row) => ({ + callHistoryUuid: row.call_history_uuid || row.id, + callId: row.call_id, + raw: row, + })); +} + +export async function getCallElement(accessToken, callElementId) { + const res = await fetch(`https://api.zoom.us/v2/phone/call_element/${callElementId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) throw new Error(`call_element failed: ${res.status}`); + return res.json(); +} +``` + +## Operational notes + +- Add explicit logging when fallback fields (`call_logs`, `call_path`) are encountered. +- Remove fallback path once migration is complete. diff --git a/partner-built/zoom-plugin/skills/phone/examples/smart-embed-postmessage-bridge.md b/partner-built/zoom-plugin/skills/phone/examples/smart-embed-postmessage-bridge.md new file mode 100644 index 00000000..b88bb310 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/examples/smart-embed-postmessage-bridge.md @@ -0,0 +1,49 @@ +# Smart Embed postMessage Bridge Pattern + +## Why this pattern + +Smart Embed event/control flow is `window.postMessage` based. Reliability depends on strict initialization order and origin validation. + +## Pattern + +```javascript +const ZOOM_ORIGIN = 'https://applications.zoom.us'; +const iframe = document.querySelector('#zoom-embeddable-phone-iframe'); + +function initSmartEmbed(config) { + iframe?.contentWindow?.postMessage({ + type: 'zp-init-config', + data: config, + }, ZOOM_ORIGIN); +} + +function makeCall(number, callerId) { + iframe?.contentWindow?.postMessage({ + type: 'zp-make-call', + data: { number, callerId, autoDial: true }, + }, ZOOM_ORIGIN); +} + +window.addEventListener('message', (event) => { + if (event.origin !== ZOOM_ORIGIN) return; + const payload = event.data; + if (!payload?.type) return; + + switch (payload.type) { + case 'zp-call-ringing-event': + case 'zp-call-connected-event': + case 'zp-call-ended-event': + case 'zp-call-log-completed-event': + handlePhoneEvent(payload); + break; + default: + break; + } +}); +``` + +## Operational notes + +- Call APIs only after iframe readiness callback. +- Persist `event.id` (if present) for idempotency. +- Keep event dispatcher tolerant to new event types. diff --git a/partner-built/zoom-plugin/skills/phone/references/call-handling-patterns.md b/partner-built/zoom-plugin/skills/phone/references/call-handling-patterns.md new file mode 100644 index 00000000..26d9a667 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/call-handling-patterns.md @@ -0,0 +1,34 @@ +# Call Handling API Patterns + +## Endpoint family + +- `POST /phone/extension/{extensionId}/call_handling/settings/{settingType}` +- `PATCH /phone/extension/{extensionId}/call_handling/settings/{settingType}` +- `GET /phone/extension/{extensionId}/call_handling/settings` + +## Supported extension targets + +- Users +- Auto receptionists +- Call queues + +## Common subsettings + +- `custom_hours` +- `holiday` +- `call_handling` +- `call_forwarding` (user-focused) + +## Practical implementation pattern + +1. Read current settings snapshot with `GET`. +2. Build small, typed patch payloads by subsetting. +3. Update business/closed/holiday hours independently. +4. Validate E.164 formatting for external phone numbers. +5. Store previous settings for rollback. + +## Drift watchpoints + +- Enum/action values may evolve. +- Routing field names differ between docs sections and old implementations. +- Keep a server-side validator to reject malformed call-handling payloads before API call. diff --git a/partner-built/zoom-plugin/skills/phone/references/crm-sample-validation.md b/partner-built/zoom-plugin/skills/phone/references/crm-sample-validation.md new file mode 100644 index 00000000..dca3910a --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/crm-sample-validation.md @@ -0,0 +1,36 @@ +# CRM Sample Validation (https://github.com/zoom/CRM-Sample) + +## Useful architecture patterns adopted + +- Smart Embed as dedicated iframe sidebar component. +- Server-only OAuth token handling with `next-auth` callbacks. +- API route pattern that reads session token and calls Phone APIs. +- Client-side event listener for Smart Embed message events. + +## Environment keys observed in sample + +- `ZOOM_CLIENT_ID` +- `ZOOM_CLIENT_SECRET` +- `NEXTAUTH_URL` +- `NEXTAUTH_SECRET` + +## Lifecycle pattern extracted + +1. User authenticates with Zoom OAuth. +2. Server stores access/refresh token session state. +3. UI renders Smart Embed iframe. +4. UI sends click-to-call command and listens for events. +5. Backend fetches call history/contact data for CRM views. + +## Contradictions and drift issues found + +- Sample still maps response via `data.call_logs` (legacy shape) while migration docs push toward call history/call element shapes. +- README references `.env.example`, repository provides `.env.sample`. +- Middleware matcher and route naming are inconsistent (`/call-log` vs `/call-logs`, missing `/api/calls/[id]` route used by modal). +- Sample contains hardcoded demo records in some screens alongside live API calls. + +## Guidance + +- Treat sample as architectural reference, not canonical API contract. +- Apply migration-safe normalizers for call history fields. +- Validate each endpoint payload against current Phone API docs. diff --git a/partner-built/zoom-plugin/skills/phone/references/deprecations-and-migrations.md b/partner-built/zoom-plugin/skills/phone/references/deprecations-and-migrations.md new file mode 100644 index 00000000..ebfc31dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/deprecations-and-migrations.md @@ -0,0 +1,30 @@ +# Deprecations and Migration Notes (Zoom Phone) + +## Timeline extracted from docs + +- Legacy Call Logs API (v1) full deprecation: **April 2026**. +- Legacy Call Log webhooks (v1) full deprecation: **May 2026**. +- Legacy array fields deprecation: +- `call_log` array deprecation: **November 2026**. +- `call_path` array deprecation: **November 2026**. + +## API migration map + +- `GET /phone/call_logs` -> `GET /phone/call_history` +- `GET /phone/call_logs/{callLogId}` -> `GET /phone/call_history/{call_history_uuid}` +- `GET /phone/call_history_detail/{callHistoryId}` -> `GET /phone/call_element/{call_element_id}` + +## Webhook migration map + +- `phone.call_log_deleted` -> `phone.call_history_deleted` -> `phone.call_element_deleted` +- `phone.callee_call_log_completed` -> `phone.callee_call_history_completed` -> `phone.callee_call_element_completed` +- `phone.caller_call_log_completed` -> `phone.caller_call_history_completed` -> `phone.caller_call_element_completed` + +## Compatibility strategy + +- Standardize storage fields: +- `call_id` +- `call_history_uuid` +- `call_element_id` +- Add adapters for old/new field names during transition windows. +- Prefer v3 naming for all new features and schemas. diff --git a/partner-built/zoom-plugin/skills/phone/references/environment-variables.md b/partner-built/zoom-plugin/skills/phone/references/environment-variables.md new file mode 100644 index 00000000..5a626ac5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/environment-variables.md @@ -0,0 +1,26 @@ +# Zoom Phone Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth app identity for Phone APIs | Zoom Marketplace -> General OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> General OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | Yes (user OAuth) | OAuth callback URL | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_ACCOUNT_ID` | Optional (S2S patterns) | Account-level service integrations | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_WEBHOOK_SECRET` or `WEBHOOK_SECRET_TOKEN` | Recommended | Webhook signature verification | Zoom Marketplace -> Features -> Event Subscriptions -> Secret Token | +| `ZOOM_PHONE_SMART_EMBED_URL` | Optional | Smart Embed iframe URL override | Zoom Phone Smart Embed docs (`applications.zoom.us` path) | +| `ZOOM_PHONE_SMART_EMBED_ORIGIN` | Recommended | Allowed postMessage origin | Set to `https://applications.zoom.us` | + +## Common runtime keys + +- `NEXTAUTH_URL` (if using NextAuth) +- `NEXTAUTH_SECRET` (if using NextAuth) +- `PORT` +- `NODE_ENV` + +## Notes + +- Keep OAuth secrets server-side only. +- Smart Embed approved domains are configured in Marketplace app settings, not in `.env`. +- Re-authorize app after changing scopes. diff --git a/partner-built/zoom-plugin/skills/phone/references/forum-top-questions.md b/partner-built/zoom-plugin/skills/phone/references/forum-top-questions.md new file mode 100644 index 00000000..939d0076 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/forum-top-questions.md @@ -0,0 +1,95 @@ +--- +title: "Forum-Derived Top Questions (Phone)" +--- + +# Forum-Derived Top Questions (Phone) + +Use this as a checklist of the most common recent Developer Forum asks for Zoom Phone integrations. + +## Fast Routing Questions (Ask First) + +- Integration surface: Smart Embed, Phone REST API, webhooks, or URI launch (`zoomphonecall://`, `zoomphonesms://`). +- App/auth type: Server-to-Server OAuth vs user OAuth, and who the token is acting as. +- Account posture: Zoom Phone license assigned, user enabled, admin permissions, site/queue scope. +- Exact failure: HTTP status + Zoom `code`/`message` + endpoint/event name + sample payload. +- Correlation IDs available: `call_id`, `call_history_uuid`, `call_element_id`, recording ID. + +## Smart Embed Sign-In or Calling Fails + +Common asks: +- Smart Embed shows login but never completes. +- Widget loads, but outbound/inbound calling does not work. +- `zp-make-call`/search-and-match behaviors are inconsistent. + +Answer pattern: +- Confirm approved Smart Embed domain matches the real runtime origin exactly. +- Confirm `origin` parameter is domain-level where required and not path-mismatched. +- Verify Zoom client sign-in state and account licensing prerequisites. +- Add strict `postMessage` origin handling and validate event init sequence. + +## `call_logs` to `call_history` Migration Gaps + +Common asks: +- Missing fields after migrating to `call_history`. +- Existing call analytics pipelines break after deprecation migration. + +Answer pattern: +- Treat migration as a schema migration, not a drop-in endpoint swap. +- Build a mapping layer from legacy fields to current call history/call element fields. +- Persist both legacy and new IDs during transition for reconciliation. +- Update downstream reports that assumed removed fields. + +## Recording and Download URL Auth Errors + +Common asks: +- `download_url` returns 401/403. +- `Invalid access token, does not contain scopes` on recordings/transcripts. + +Answer pattern: +- Generate a fresh token from the app that owns the needed scopes. +- Re-authorize after scope changes; verify token scope set, not just app config. +- Handle redirects while preserving auth headers where needed. +- Keep a fallback retry path for temporary scope/permission regressions. + +## "Zoom Phone Has Not Been Enabled" (`2013`/`2031`) + +Common asks: +- Token works for some APIs/users but Phone endpoints return not enabled. + +Answer pattern: +- Verify `account_id` is present in S2S token request and token is from expected account. +- Verify target users actually have Zoom Phone entitlement. +- Verify caller/admin context has permission for account-level Phone resources. +- Re-test with one known-good licensed admin and one known-good licensed user. + +## Webhooks: Missing Events or Duplicates + +Common asks: +- Expected call events not received. +- Missed-call events delivered more than once. + +Answer pattern: +- Acknowledge webhooks quickly with `200`/`204` and process asynchronously. +- Implement idempotency keyed by event ID/call identifiers. +- Expect retries and occasional ordering variance. +- Validate event subscription scope and verify webhook logs before blaming delivery. + +## Correlating Calls Across APIs and Events + +Common asks: +- Hard to tie recordings, call path/history, and webhook events to one interaction. + +Answer pattern: +- Persist all call identifiers emitted at each lifecycle phase. +- Build a correlation table keyed by your internal interaction ID. +- Do not rely on a single identifier across all endpoints. + +## Pagination and Incomplete Result Sets + +Common asks: +- `/phone/users` or call list endpoints appear to miss records. + +Answer pattern: +- Always iterate `next_page_token` until exhausted. +- Keep query filters stable between page requests. +- Add dedupe + page-audit logging to detect loops or repeated pages. diff --git a/partner-built/zoom-plugin/skills/phone/references/smart-embed-event-contract.md b/partner-built/zoom-plugin/skills/phone/references/smart-embed-event-contract.md new file mode 100644 index 00000000..01a9d413 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/smart-embed-event-contract.md @@ -0,0 +1,37 @@ +# Smart Embed Event Contract + +## Core initialization and command messages + +- `zp-init-config` +- `zp-make-call` +- `zp-input-sms` +- `zp-contact-search-response` +- `zp-contact-match-response` + +## Core emitted event types + +- `zp-call-ringing-event` +- `zp-call-connected-event` +- `zp-call-ended-event` +- `zp-call-log-completed-event` +- `zp-call-recording-completed-event` +- `zp-call-voicemail-received-event` +- `zp-ai-call-summary-event` +- `zp-sms-log-event` +- `zp-save-log-event` +- `zp-contact-search-event` +- `zp-contact-match-event` +- `zp-notes-save-event` + +## Field-level reliability notes + +- `callId` appears early in lifecycle. +- `callLogId` appears in completion-oriented events. +- `event.id` can be used for deduplication/idempotency. +- Additional flags can appear (for example `enableAutoLog` behavior fields). + +## Security and resilience + +- Validate `event.origin === https://applications.zoom.us`. +- Keep a permissive parser for new optional fields. +- Route unknown event types into structured logs, not hard failures. diff --git a/partner-built/zoom-plugin/skills/phone/references/source-map.md b/partner-built/zoom-plugin/skills/phone/references/source-map.md new file mode 100644 index 00000000..6462d864 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/references/source-map.md @@ -0,0 +1,28 @@ +# Zoom Phone Source Map + +Crawled docs source: +- `https://developers.zoom.us/docs/phone/` +- Crawl config used: depth `10`, concurrency `10`, Android excluded. + +## Processed pages + +- `call-data.md` +- `call-handling.md` +- `create-app.md` +- `first-app.md` +- `integrate-with-zoom-phone.md` +- `migrate.md` +- `outbound-call.md` +- `outbound-sms.md` +- `smart-embed-guide.md` +- `smart-embed.md` +- `start.md` +- `webhook-migrate.md` + +## Mapping to skill docs + +- App setup + OAuth -> [../SKILL.md](../SKILL.md), [environment-variables.md](environment-variables.md) +- Smart Embed lifecycle/events -> `examples/smart-embed-postmessage-bridge.md`, `references/smart-embed-event-contract.md` +- Call handling admin API -> `references/call-handling-patterns.md` +- API/webhook migration timeline -> `references/deprecations-and-migrations.md` +- CRM sample validation -> `references/crm-sample-validation.md` diff --git a/partner-built/zoom-plugin/skills/phone/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/phone/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..1b7c5865 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/scenarios/high-level-scenarios.md @@ -0,0 +1,33 @@ +# Zoom Phone High-Level Scenarios + +## 1) Smart Embed CRM Softphone + +Use Smart Embed in a CRM sidebar to place and receive calls, then log outcomes back into CRM records. + +## 2) Click-to-Call from Lead Table + +Use `zp-make-call` or URI scheme launch from contact rows; subscribe to call status events for UI updates. + +## 3) SMS Follow-Up Automation + +Use `zoomphonesms://` and Smart Embed SMS events to trigger follow-up tasks and SLA timers. + +## 4) Call Disposition + Notes Pipeline + +Use `zp-save-log-event` and `zp-notes-save-event` to capture custom dispositions and sync to third-party systems. + +## 5) Real-Time Supervisor Dashboard + +Use `phone.*` webhooks and call events to track active calls, misses, rejects, and queue pressure. + +## 6) Call History Modernization + +Migrate from legacy call log fields to call history/call element IDs while maintaining backward compatibility for old records. + +## 7) Call Handling Admin Automation + +Use call handling APIs to standardize business/closed/holiday routing for users, auto receptionists, and call queues. + +## 8) Blended Phone + Contact Center Journey + +Route phone interactions into Contact Center follow-up or escalation workflows using shared CRM context. diff --git a/partner-built/zoom-plugin/skills/phone/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/phone/troubleshooting/common-issues.md new file mode 100644 index 00000000..fcc44ec1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/phone/troubleshooting/common-issues.md @@ -0,0 +1,39 @@ +# Zoom Phone Common Issues + +## Smart Embed event listener gets nothing + +Checks: +- Iframe is loaded from `https://applications.zoom.us`. +- `onZoomPhoneIframeApiReady` sequence is respected. +- `postMessage` origin checks are correct. +- Approved domains configured in Zoom Phone Smart Embed app settings. + +## OAuth works but API calls fail (401/403) + +Checks: +- Required scopes are present and app was re-authorized. +- Access token is current (refresh flow works). +- Right app type and account context are used. + +## Data fields missing after migration + +Checks: +- Code expects old fields (`call_logs`, `call_path`) only. +- Endpoint path still points to legacy call log URLs. +- Webhook processor supports `call_element_id` fields. + +## URI launch inconsistencies + +Checks: +- Client is installed and signed in. +- Scheme is valid (`callto:`, `tel:`, `zoomphonecall://`, `zoomphonesms://`). +- Platform caveats are handled. +- Android caveat: docs explicitly note no `zoomphonecall`/`tel` support due to system limitations. + +## Call handling API patch fails + +Checks: +- `extensionId` target type is correct. +- Payload subsetting matches endpoint context. +- Phone numbers are E.164 where required. +- Enum/action values are valid for current API version. diff --git a/partner-built/zoom-plugin/skills/plan-zoom-integration/SKILL.md b/partner-built/zoom-plugin/skills/plan-zoom-integration/SKILL.md new file mode 100644 index 00000000..e89321fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/plan-zoom-integration/SKILL.md @@ -0,0 +1,42 @@ +--- +name: plan-zoom-integration +description: Turn a Zoom integration idea into an implementation plan with architecture, auth, and delivery milestones. Use when you need a practical build plan, phased delivery sequence, risk list, and next-step recommendation. +argument-hint: "" +user-invocable: false +--- + +# /plan-zoom-integration + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Create a practical build plan for a Zoom integration or app. + +## Usage + +```text +/plan-zoom-integration $ARGUMENTS +``` + +## Workflow + +1. Capture the target user flow and success criteria. +2. Choose the correct Zoom surface and supporting services. +3. Define auth requirements, scopes, and account assumptions. +4. Break implementation into phases: prototype, core integration, reliability, and launch. +5. Call out hard risks early: OAuth setup, webhook verification, SDK environment limits, marketplace review, or MCP client constraints. +6. End with the smallest deliverable that proves the architecture. + +## Output + +- Architecture summary +- Zoom products and APIs required +- Auth and scope checklist +- Delivery phases +- Risks, open questions, and immediate next action + +## Related Skills + +- [start](../start/SKILL.md) +- [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) +- [build-zoom-meeting-app](../build-zoom-meeting-app/SKILL.md) +- [build-zoom-bot](../build-zoom-bot/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/plan-zoom-product/SKILL.md b/partner-built/zoom-plugin/skills/plan-zoom-product/SKILL.md new file mode 100644 index 00000000..3d3a745a --- /dev/null +++ b/partner-built/zoom-plugin/skills/plan-zoom-product/SKILL.md @@ -0,0 +1,41 @@ +--- +name: plan-zoom-product +description: Choose the right Zoom building surface for a use case and explain the tradeoffs clearly. Use when deciding between REST API, Webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Phone, Contact Center, or MCP for a specific product idea or integration goal. +argument-hint: "" +user-invocable: false +--- + +# /plan-zoom-product + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Choose between Zoom REST API, Webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Phone, Contact Center, or MCP for a specific use case. + +## Usage + +```text +/plan-zoom-product $ARGUMENTS +``` + +## Workflow + +1. Identify the user's actual goal. +2. Classify whether the problem is automation, embedded meetings, custom video, in-client app behavior, event delivery, AI tooling, or support/phone/contact-center work. +3. If the request is ambiguous, ask one short clarifier before locking the recommendation. +4. Recommend the primary Zoom surface and list the minimum supporting pieces. +5. Explain why the rejected alternatives are worse for this case. +6. End with a concrete next-step plan. + +## Output + +- Recommended Zoom surface +- Supporting components required +- Key tradeoffs and constraints +- Suggested implementation sequence +- Relevant skill links for the next step + +## Related Skills + +- [start](../start/SKILL.md) +- [choose-zoom-approach](../choose-zoom-approach/SKILL.md) +- [design-mcp-workflow](../design-mcp-workflow/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/probe-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/probe-sdk/RUNBOOK.md new file mode 100644 index 00000000..e7bad23a --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/RUNBOOK.md @@ -0,0 +1,67 @@ +# Probe SDK 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- API/option naming can drift by version; validate against current Probe SDK reference. + +## 1) Confirm Integration Surface + +- Confirm this is a web diagnostics use case, not meeting/session join runtime. +- Confirm whether you need only device checks, only network checks, or full diagnostics. +- Confirm renderer target strategy (`video-tag` or canvas-based renderer). + +## 2) Confirm Required Inputs + +- No Zoom Marketplace credentials are required for core Probe SDK diagnostics. +- Device IDs are required for explicit audio input/output and camera diagnostics. +- For comprehensive network diagnostics, verify optional JS/WASM URL override strategy. + +## 3) Confirm Lifecycle Order + +1. `requestMediaDevicePermission()`. +2. `requestMediaDevices()`. +3. `diagnoseAudio(...)` / `diagnoseVideo(...)`. +4. `startToDiagnose(jsUrl, wasmUrl, config, statsListener)`. +5. `stopToDiagnose*` and `cleanup()` on exit. + +## 4) Confirm Event/State Handling + +- Keep stream lifecycle explicit (`releaseMediaStream`). +- Keep stats callback lightweight and avoid blocking UI thread. +- Persist final report snapshot before cleanup. + +## 5) Confirm Cleanup + Upgrade Posture + +- Always stop active diagnostics before page unload/navigation. +- Re-check renderer option naming and report field names on upgrades. +- Re-check browser compatibility assumptions against current docs. + +## 6) Quick Probes + +- Permissions prompt appears and resolves expectedly. +- Devices list includes expected microphone/speaker/camera. +- Video diagnostic renders to selected target. +- Network diagnostic emits stats and final report. + +## 7) Fast Decision Tree + +- No media diagnostics -> permissions denied or insecure context. +- Video diagnostics fail -> renderer/target mismatch or unsupported renderer type. +- Network diagnostic incomplete -> timeout/domain/config mismatch. +- Report schema mismatch -> version drift between docs and installed package. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/probe-sdk/ +- https://marketplacefront.zoom.us/sdk/probe/index.html + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/probe-sdk/` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/` diff --git a/partner-built/zoom-plugin/skills/probe-sdk/SKILL.md b/partner-built/zoom-plugin/skills/probe-sdk/SKILL.md new file mode 100644 index 00000000..3d4a94eb --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/SKILL.md @@ -0,0 +1,81 @@ +--- +name: probe-sdk +description: "Reference skill for Zoom Probe SDK. Use after routing to a preflight workflow when testing browser compatibility, media permissions, audio or video diagnostics, and network readiness before users join." +user-invocable: false +triggers: + - "probe sdk" + - "zoom probe" + - "@zoom/probesdk" + - "media diagnostics" + - "network diagnostic" + - "preflight check" + - "diagnose audio video" + - "browser compatibility diagnostics" + - "diagnostic report" +--- + +# Zoom Probe SDK + +Background reference for preflight diagnostics on user devices and networks before meeting or session workflows. + +Official docs: +- https://developers.zoom.us/docs/probe-sdk/ +- https://marketplacefront.zoom.us/sdk/probe/index.html + +Reference sample: +- https://github.com/zoom/probesdk-web + +## Routing Guardrail + +- Use Probe SDK when the user needs client-side diagnostics and readiness scoring (device/network/browser capability), not meeting/session join. +- If user needs embedded meeting flows, route to [../meeting-sdk/SKILL.md](../meeting-sdk/SKILL.md). +- If user needs custom real-time session UX, route to [../video-sdk/SKILL.md](../video-sdk/SKILL.md). +- If user needs backend orchestration of events/APIs, chain with [../rivet-sdk/SKILL.md](../rivet-sdk/SKILL.md), [../oauth/SKILL.md](../oauth/SKILL.md), and [../rest-api/SKILL.md](../rest-api/SKILL.md). + +## Quick Links + +Start here: +1. [probe-sdk.md](probe-sdk.md) +2. [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +3. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +4. [examples/diagnostic-page-pattern.md](examples/diagnostic-page-pattern.md) +5. [examples/comprehensive-network-pattern.md](examples/comprehensive-network-pattern.md) +6. [references/probe-reference-map.md](references/probe-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [references/samples-validation.md](references/samples-validation.md) +10. [references/source-map.md](references/source-map.md) +11. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) +12. [RUNBOOK.md](RUNBOOK.md) + +## Common Lifecycle Pattern + +1. Initialize `Prober` / `Reporter`. +2. Request media permissions and enumerate devices. +3. Run targeted diagnostics (`diagnoseAudio`, `diagnoseVideo`). +4. Run comprehensive network diagnostic (`startToDiagnose`) and stream stats to UI. +5. Produce final report and apply readiness gates. +6. Stop/cleanup (`stopToDiagnose`, `stopToDiagnoseVideo`, `releaseMediaStream`, `cleanup`). + +## High-Level Scenarios + +- Pre-join diagnostics page before Meeting SDK join action. +- Support workflow that captures structured report for customer troubleshooting. +- Device certification flow for kiosk or controlled endpoint environments. +- Browser capability gating for advanced media features. + +See [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) for details. + +## Chaining + +- Meeting pre-join gate: [../meeting-sdk/web/SKILL.md](../meeting-sdk/web/SKILL.md) +- Video session readiness gate: [../video-sdk/web/SKILL.md](../video-sdk/web/SKILL.md) +- Telemetry/report ingestion backend: [../rivet-sdk/SKILL.md](../rivet-sdk/SKILL.md) + [../rest-api/SKILL.md](../rest-api/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for optional `.env` keys and how to source values. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/concepts/architecture-and-lifecycle.md b/partner-built/zoom-plugin/skills/probe-sdk/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..a624dac1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/concepts/architecture-and-lifecycle.md @@ -0,0 +1,57 @@ +# Probe SDK Architecture and Lifecycle + +## Purpose + +Probe SDK answers one question before real-time media starts: +- `Can this user/device/network support an acceptable experience?` + +## Architecture Model + +```text +User Browser + -> Probe SDK (Prober, Reporter) + -> Media APIs (permissions/devices) + -> Renderer path (video tag / WebGL / WebGL2 / WebGPU) + -> Network probing runtime (JS/WASM + domain endpoint) + -> Diagnostic stats stream + final report + -> UI gating decision (allow join / warn / block) +``` + +## Lifecycle Workflow + +1. Initialize +- `const prober = new Prober()` +- `const reporter = new Reporter()` (optional for standalone feature/basic reports) + +2. Permissions and devices +- `requestMediaDevicePermission({ audio: true, video: true })` +- `requestMediaDevices()` + +3. Targeted diagnostics +- `diagnoseAudio(inputConstraints, outputConstraints, duration)` +- `diagnoseVideo(constraints, { rendererType, target })` + +4. Comprehensive diagnostics +- `startToDiagnose(jsUrl, wasmUrl, config, statsListener)` +- stream live stats and wait for final report payload + +5. Tear-down and cleanup +- `stopToDiagnose()` / `stopToDiagnoseVideo(stream?)` +- `releaseMediaStream(stream)` +- `cleanup()` + +## Readiness Policy Calibration + +- Keep readiness policy product-specific and versioned (for example, `policy_version=2026-02`). +- Define explicit thresholds per output signal (`allow`, `warn`, `block`) for network/audio/video outcomes. +- Recalibrate policy thresholds whenever upgrading Probe SDK or changing browser support baseline. +- Log policy version with each final report so support can reproduce decisions. + +## Data Model Notes + +Typical final report includes: +- network diagnostic result +- basic info entries +- supported feature entries + +Field naming may vary by version (`basicInfo` vs `basicInfoEntries`, `supportedFeatures` vs `featureEntries`), so version-aware adapters are recommended. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/examples/comprehensive-network-pattern.md b/partner-built/zoom-plugin/skills/probe-sdk/examples/comprehensive-network-pattern.md new file mode 100644 index 00000000..48f67292 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/examples/comprehensive-network-pattern.md @@ -0,0 +1,40 @@ +# Comprehensive Network Diagnostic Pattern + +```javascript +import { Prober } from "@zoom/probesdk"; + +const prober = new Prober(); + +export async function runComprehensiveDiagnostic() { + const jsUrl = ""; // optional custom hosted prober.js + const wasmUrl = ""; // optional custom hosted prober.wasm loader + + const config = { + probeDuration: 120 * 1000, + connectTimeout: 20 * 1000, + domain: "zoom.us", + }; + + const statsHistory = []; + + const report = await prober.startToDiagnose(jsUrl, wasmUrl, config, (stats) => { + statsHistory.push(stats); + }); + + return { + report, + statsHistory, + }; +} + +export async function stopEarlyAndCollect() { + const partial = await prober.stopToDiagnose(); + prober.cleanup(); + return partial; +} +``` + +## Notes + +- Use stats callback for realtime charting; keep callback lightweight. +- Wrap final report fields behind adapter layer for version drift. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/examples/diagnostic-page-pattern.md b/partner-built/zoom-plugin/skills/probe-sdk/examples/diagnostic-page-pattern.md new file mode 100644 index 00000000..d7c14922 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/examples/diagnostic-page-pattern.md @@ -0,0 +1,42 @@ +# Diagnostic Page Pattern + +```javascript +import { Prober } from "@zoom/probesdk"; + +const prober = new Prober(); + +export async function runDeviceChecks(videoCanvas) { + const permission = await prober.requestMediaDevicePermission({ audio: true, video: true }); + if (permission.error) return { ok: false, stage: "permission", error: permission.error }; + + const devices = await prober.requestMediaDevices(); + if (devices.error) return { ok: false, stage: "devices", error: devices.error }; + + const cameraId = devices.devices?.find((d) => d.kind === "videoinput")?.deviceId || "default"; + const micId = devices.devices?.find((d) => d.kind === "audioinput")?.deviceId || "default"; + const speakerId = devices.devices?.find((d) => d.kind === "audiooutput")?.deviceId; + + const audioResult = await prober.diagnoseAudio( + { audio: { deviceId: micId }, video: false }, + { audio: { deviceId: speakerId }, video: false }, + 5000 + ); + + const videoResult = await prober.diagnoseVideo( + { video: { deviceId: cameraId } }, + { rendererType: 2, target: videoCanvas } + ); + + return { ok: true, devices: devices.devices, audioResult, videoResult }; +} + +export function cleanupStream(stream) { + prober.releaseMediaStream(stream); + prober.cleanup(); +} +``` + +## Notes + +- `rendererType` target requirements must match chosen renderer. +- Always release media streams when diagnostics are complete. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/probe-sdk.md b/partner-built/zoom-plugin/skills/probe-sdk/probe-sdk.md new file mode 100644 index 00000000..1b1514fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/probe-sdk.md @@ -0,0 +1,15 @@ +# Zoom Probe SDK (Overview) + +Probe SDK is a web diagnostics SDK used to validate media devices, network quality, and browser capability before meeting/session workflows. + +For full documentation and navigation, start at [SKILL.md](SKILL.md). + +## Quick Links + +- [Architecture and Lifecycle](concepts/architecture-and-lifecycle.md) +- [High-Level Scenarios](scenarios/high-level-scenarios.md) +- [Diagnostic Page Pattern](examples/diagnostic-page-pattern.md) +- [Comprehensive Network Pattern](examples/comprehensive-network-pattern.md) +- [Probe Reference Map](references/probe-reference-map.md) +- [Sample Validation](references/samples-validation.md) +- [Common Issues](troubleshooting/common-issues.md) diff --git a/partner-built/zoom-plugin/skills/probe-sdk/references/environment-variables.md b/partner-built/zoom-plugin/skills/probe-sdk/references/environment-variables.md new file mode 100644 index 00000000..c3e84468 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/references/environment-variables.md @@ -0,0 +1,23 @@ +# Probe SDK Environment Variables + +Probe SDK does not require Zoom Marketplace credentials for core diagnostics. + +## Required `.env` keys + +- None required by the SDK itself. + +## Optional `.env` keys (app-level conventions) + +| Key | Required | Description | Where to find value | +|-----|----------|-------------|---------------------| +| `PROBE_JS_URL` | Optional | Override URL for probe runtime JS | Hosted by your app/infrastructure (or empty to use defaults) | +| `PROBE_WASM_URL` | Optional | Override URL for probe runtime WASM loader | Hosted by your app/infrastructure (or empty to use defaults) | +| `PROBE_DOMAIN` | Optional | Domain target for diagnostic probes | Usually `zoom.us` or your approved diagnostic domain | +| `PROBE_DURATION_MS` | Optional | Probe duration in milliseconds | Chosen by your product policy (defaults align with SDK guidance) | +| `PROBE_CONNECT_TIMEOUT_MS` | Optional | Probe connect timeout in milliseconds | Chosen by your product policy | + +## Notes + +- Because no OAuth credentials are required, Probe SDK is suitable as a lightweight preflight page before auth-sensitive flows. +- Do not require `ZOOM_CLIENT_ID`, `ZOOM_CLIENT_SECRET`, or account-level OAuth tokens for core Probe diagnostics. +- Keep optional URLs/versioning aligned with package version to avoid JS/WASM mismatch. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/references/probe-reference-map.md b/partner-built/zoom-plugin/skills/probe-sdk/references/probe-reference-map.md new file mode 100644 index 00000000..dc7fd593 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/references/probe-reference-map.md @@ -0,0 +1,43 @@ +# Probe SDK Reference Map + +## Canonical Documentation + +- Product docs: https://developers.zoom.us/docs/probe-sdk/ +- Get started: https://developers.zoom.us/docs/probe-sdk/get-started/ +- API reference: https://marketplacefront.zoom.us/sdk/probe/index.html + +## Core Classes + +- `Prober` +- `Reporter` + +## Core Methods + +From global/class reference surfaces: +- `requestMediaDevicePermission` +- `requestMediaDevices` +- `diagnoseAudio` +- `diagnoseVideo` +- `startToDiagnose` +- `stopToDiagnose` +- `stopToDiagnoseVideo` +- `releaseMediaStream` +- `reportBasicInfo` +- `reportFeatures` +- `cleanup` + +## Key Enums/Constants + +- `RENDERER_TYPE` +- `NETWORK_QUALITY_LEVEL` +- `BANDWIDTH_QUALITY_LEVEL` +- `PROTOCOL_TYPE` +- `ERR_CODE` +- `SUPPORTED_FEATURE_INDEX` +- `BASIC_INFO_ATTR_INDEX` + +## Changelog and Package + +- Changelog: https://developers.zoom.us/changelog/probe-sdk/ +- npm package: https://www.npmjs.com/package/@zoom/probesdk +- sample source: https://github.com/zoom/probesdk-web diff --git a/partner-built/zoom-plugin/skills/probe-sdk/references/samples-validation.md b/partner-built/zoom-plugin/skills/probe-sdk/references/samples-validation.md new file mode 100644 index 00000000..10cfc632 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/references/samples-validation.md @@ -0,0 +1,25 @@ +# Probe SDK Sample Validation and Drift Notes + +Validated sample: +- `zoom/probesdk-web` + +## Lifecycle and Architecture Patterns Confirmed + +- `Prober` initialization followed by staged diagnostics. +- Explicit separation between targeted checks (`diagnoseAudio`, `diagnoseVideo`) and comprehensive network probe (`startToDiagnose`). +- Cleanup and stream lifecycle methods are critical for stable page behavior. + +## Contradictions and Drift Indicators + +- Doc snippet drift: +- Some docs use video options key `type` while reference/sample prefer `rendererType`. +- Report shape drift: +- Docs show `basicInfo`/`supportedFeatures` whereas sample README also references `basicInfoEntries`/`featureEntries`. +- Timeout example variance: +- Docs and sample show different `connectTimeout` defaults in code snippets. + +## Recommendations + +- Use adapter layer for diagnostic report fields. +- Normalize renderer options through a shared utility in your app. +- Keep browser matrix in your own QA docs and update quarterly. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/references/source-map.md b/partner-built/zoom-plugin/skills/probe-sdk/references/source-map.md new file mode 100644 index 00000000..194a0858 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/references/source-map.md @@ -0,0 +1,13 @@ +# Probe SDK Source Map + +## Crawled Docs (local raw-docs) + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/probe-sdk/get-started.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/index.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/Prober.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/Reporter.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/global.md` + +## External Validation Source + +- `zoom/probesdk-web` diff --git a/partner-built/zoom-plugin/skills/probe-sdk/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/probe-sdk/references/versioning-and-compatibility.md new file mode 100644 index 00000000..1c60278e --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/references/versioning-and-compatibility.md @@ -0,0 +1,24 @@ +# Probe SDK Versioning and Compatibility + +## Upgrade Strategy + +Use the standard upgrade workflow: +- [../../general/references/sdk-upgrade-workflow.md](../../general/references/sdk-upgrade-workflow.md) + +## Compatibility Risks + +- Renderer option naming drift (`type` vs `rendererType`) across docs/examples. +- Report object field naming drift (`basicInfo` vs `basicInfoEntries`, `supportedFeatures` vs `featureEntries`). +- Browser support table age vs current browser versions. +- Runtime JS/WASM URL override mismatches. + +## Safe Upgrade Checklist + +- Pin and record current/target `@zoom/probesdk` version. +- Compare get-started docs, API reference, and sample repo behavior. +- Validate all renderer targets in your browser matrix. +- Validate both full diagnostic completion and early stop path. +- Validate report schema adapter and downstream consumers. +- Pin JS/WASM assets to the same Probe SDK release and prevent mixed-version loading. +- Add cache-busting strategy for JS/WASM updates (asset fingerprinting or versioned URLs). +- Confirm CDN/browser cache invalidation behavior before production rollout. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/probe-sdk/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..c26e094f --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/scenarios/high-level-scenarios.md @@ -0,0 +1,31 @@ +# Probe SDK High-Level Scenarios + +## 1) Meeting Pre-Join Readiness Gate + +- Run Probe diagnostics before showing Meeting SDK join button. +- Block join for severe failures (no media permissions, fatal network score). +- Show guidance and retry path for recoverable failures. + +## 2) Video Session Quality Predictor + +- Pair Probe results with Video SDK session UX. +- Choose default video quality and renderer path based on capability checks. +- Fall back to audio-first profile when network quality is poor. + +## 3) Support/Triage Diagnostic Capture + +- Collect final diagnostic report for helpdesk workflows. +- Attach report to support ticket to reduce reproduction time. +- Compare report against known-good baseline by browser/OS cohort. + +## 4) Managed Device Certification + +- Run automated checks across approved browser/device matrix. +- Persist pass/fail score and feature support profile. +- Use certification output to guide endpoint policy and rollout. + +## 5) Incident Response Validation + +- During outage/performance alerts, run Probe tests from affected geos. +- Distinguish local device failures from Zoom/service-zone path issues. +- Route incident response based on network and protocol-specific findings. diff --git a/partner-built/zoom-plugin/skills/probe-sdk/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/probe-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..3c933dd6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/probe-sdk/troubleshooting/common-issues.md @@ -0,0 +1,65 @@ +# Probe SDK Common Issues + +## Permissions denied or missing streams + +Symptoms: +- Permission request returns error. +- No stream returned for diagnostics. + +Checks: +- HTTPS/secure context is used. +- Not in insecure context (`http://`, mixed content, blocked iframe permissions policy). +- Browser-level camera/mic permissions are granted. +- Device is not locked by another app. + +## No media devices detected + +Symptoms: +- `requestMediaDevices` returns empty sets for mic/camera/speaker. + +Checks: +- OS-level privacy settings allow browser device access. +- External USB/Bluetooth devices are connected before page load. +- Virtual device drivers are installed and recognized by the browser. +- Browser enterprise policies are not blocking media device enumeration. + +## Video diagnostic fails to render + +Symptoms: +- `diagnoseVideo` error or blank target. + +Checks: +- Renderer option key and target match selected renderer. +- `video-tag` renderer uses HTMLVideoElement target. +- WebGL/WebGL2/WebGPU renderers use canvas/offscreen canvas target. + +## Network diagnostic never completes + +Symptoms: +- `startToDiagnose` does not return final report in expected duration. + +Checks: +- `probeDuration` and `connectTimeout` values are reasonable. +- Domain and optional JS/WASM URLs are reachable. +- Browser/network policies do not block probing paths. + +## Report field mismatch in app code + +Symptoms: +- Undefined fields when parsing final report. + +Checks: +- Add compatibility adapter for `basicInfo` vs `basicInfoEntries`. +- Add compatibility adapter for `supportedFeatures` vs `featureEntries`. +- Pin SDK version and align parser tests to that version. + +## Residual resource usage after diagnostics + +Symptoms: +- Camera indicator remains active. +- Memory/network usage persists after leaving page. + +Checks: +- Call `stopToDiagnoseVideo` and/or `releaseMediaStream`. +- Call `stopToDiagnose` on early exit. +- Call `cleanup()` on route/page teardown. diff --git a/partner-built/zoom-plugin/skills/rest-api/RUNBOOK.md b/partner-built/zoom-plugin/skills/rest-api/RUNBOOK.md new file mode 100644 index 00000000..53924091 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/RUNBOOK.md @@ -0,0 +1,89 @@ +# REST API 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom REST API integration failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Auth Flow and Endpoint + +- Choose matching OAuth flow for use case (S2S/User/PKCE/Device). +- Use token URL `https://zoom.us/oauth/token`. + +Wrong flow or token endpoint causes immediate auth failures. + +## 2) Confirm Scope and Account Context + +- Verify token contains required scopes. +- For admin/account-level operations, verify app/account permissions. +- Re-authorize after scope changes. + +## 3) Confirm ID Semantics + +- Distinguish Meeting ID vs Meeting UUID. +- Apply required URL encoding (double-encoding for UUID where needed). + +### ID Sanity Rules + +- Use numeric Meeting ID for many standard meeting operations. +- Use Meeting UUID for some past-instance/recording/report operations. +- If endpoint docs mention UUID and your value contains `/` or `+`, encode carefully. + +If a resource "exists" in UI but API returns not found, ID type/encoding mismatch is a top cause. + +## 4) Confirm Pagination and Rate Limits + +- Handle `next_page_token` where applicable. +- Implement retry/backoff on 429 and transient 5xx. + +### Minimal Retry Policy + +- 429 or 5xx: exponential backoff with jitter. +- Respect retry headers when provided. +- Put high-volume endpoints behind queue/batch workers. + +## 5) Confirm Webhook-Driven Workflows + +- If pipeline is event-driven, validate webhook signatures and retry behavior. +- Respond quickly and process asynchronously. + +## 6) Quick Probes + +- `GET /v2/users/me` succeeds with current token. +- Representative endpoint (e.g., list meetings) returns expected schema. +- Error payload includes actionable code/details (not HTML response). + +### Copy/Paste Validation Commands + +```bash +# 1) Get S2S access token +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) Validate token can access account context +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" + +# 3) List meetings for current user (quick schema sanity) +curl -X GET "https://api.zoom.us/v2/users/me/meetings?page_size=30" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" +``` + +Expected: JSON responses with HTTP 200 (or clear JSON error codes), not HTML error pages. + +## 7) Fast Decision Tree + +- **401/invalid token** -> wrong flow, expired token, or scope mismatch. +- **404-like behavior** -> wrong endpoint path/version or wrong resource ID. +- **429 spikes** -> missing backoff/queue strategy. + +## 8) Common Integration Mixups + +- REST `join_url` is a browser link, not a Meeting SDK join payload. +- REST API creates/manages Zoom resources; Meeting SDK and Video SDK are separate integration surfaces. +- If auth works but operation fails, check scope and resource ownership before endpoint debugging. diff --git a/partner-built/zoom-plugin/skills/rest-api/SKILL.md b/partner-built/zoom-plugin/skills/rest-api/SKILL.md new file mode 100644 index 00000000..b2a3f1e5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/SKILL.md @@ -0,0 +1,594 @@ +--- +name: build-zoom-rest-api-app +description: "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, or API error debugging." +triggers: + - "api call" + - "rest api" + - "api create meeting" + - "api get meeting" + - "api list meetings" + - "api update meeting" + - "api delete meeting" + - "meeting endpoint" + - "/v2/meetings" + - "create user" + - "zoom api" + - "api endpoint" + - "recordings api" + - "users api" + - "webinars api" + - "server-to-server oauth" + - "account_credentials" + - "account id" + - "invalid access token" + - "does not contain scopes" + - "access token is expired" + - "webhook verification" + - "crc" + - "download_url" +--- + +# /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//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](RUNBOOK.md) +- 401 Unauthorized → [Authentication Flows](concepts/authentication-flows.md) (check token expiry, scopes) +- 429 Too Many Requests → [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) +- Error codes → [Common Errors](troubleshooting/common-errors.md) +- Pagination confusion → [Common Issues](troubleshooting/common-issues.md) +- Webhooks not arriving → [Webhook Server](examples/webhook-server.md) +- Forum-derived FAQs → [Forum Top Questions](troubleshooting/forum-top-questions.md) +- Token/scope failures → [Token + Scope Playbook](troubleshooting/token-scope-playbook.md) + +**Building event-driven integrations?** +- [Webhook Server](examples/webhook-server.md) - Express.js server with CRC validation +- [Recording Pipeline](examples/recording-pipeline.md) - Auto-download via webhook events + +## Quick Start + +### Get an Access Token (Server-to-Server OAuth) + +```bash +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: +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Create a Meeting + +```bash +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 + +```bash +curl "https://api.zoom.us/v2/users?page_size=300&status=active" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +## Base URL + +``` +https://api.zoom.us/v2 +``` + +### Regional 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](https://marketplace.zoom.us/) +- 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. + +```javascript +// 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 `me` instead of `userId` (otherwise: invalid token error) +- **Server-to-Server OAuth apps**: MUST NOT use `me` — provide the actual `userId` or email +- **Account-level OAuth apps**: Can use either `me` or `userId` + +### ⚠️ Meeting ID vs UUID — Double Encoding + +UUIDs that begin with `/` or contain `//` must be **double URL-encoded**: + +```javascript +// 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 the `Z` suffix) +- `yyyy-MM-ddTHH:mm:ss` — **Local time** (no `Z`, uses `timezone` field) +- 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: + +```bash +curl -L -H "Authorization: Bearer ACCESS_TOKEN" "https://zoom.us/rec/download/..." +``` + +### Use Webhooks Instead of Polling + +```javascript +// 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, `me` keyword, 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](https://github.com/zoom/oauth-sample-app) | +| S2S OAuth Starter | [server-to-server-oauth-starter-api](https://github.com/zoom/server-to-server-oauth-starter-api) | +| User OAuth | [user-level-oauth-starter](https://github.com/zoom/user-level-oauth-starter) | +| S2S Token | [server-to-server-oauth-token](https://github.com/zoom/server-to-server-oauth-token) | +| Rivet Library | [rivet-javascript](https://github.com/zoom/rivet-javascript) | +| WebSocket Sample | [websocket-js-sample](https://github.com/zoom/websocket-js-sample) | +| Webhook Sample | [webhook-sample-node.js](https://github.com/zoom/webhook-sample-node.js) | +| Python S2S | [server-to-server-python-sample](https://github.com/zoom/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](RUNBOOK.md) + +2. **Understand the API design** → [concepts/api-architecture.md](concepts/api-architecture.md) + - Base URLs, regional endpoints, `me` keyword rules + - Meeting ID vs UUID, double-encoding, time formats + +3. **Set up authentication** → [concepts/authentication-flows.md](concepts/authentication-flows.md) + - Server-to-Server OAuth (backend automation) + - User OAuth with PKCE (user-facing apps) + - Cross-reference: [zoom-oauth](../oauth/SKILL.md) + +4. **Create your first meeting** → [examples/meeting-lifecycle.md](examples/meeting-lifecycle.md) + - Full CRUD with curl and Node.js examples + - Webhook event integration + +5. **Handle rate limits** → [concepts/rate-limiting-strategy.md](concepts/rate-limiting-strategy.md) + - Plan-based limits, retry patterns, request queuing + +6. **Set up webhooks** → [examples/webhook-server.md](examples/webhook-server.md) + - CRC validation, signature verification, event handling + +7. **Troubleshoot issues** → [troubleshooting/common-issues.md](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](concepts/api-architecture.md) - Base URL, time formats +2. [Meeting Lifecycle](examples/meeting-lifecycle.md) - Full CRUD + webhook events +3. [Meetings Reference](references/meetings.md) - All endpoints, types, settings + +### I want to manage users programmatically +1. [User Management](examples/user-management.md) - CRUD, pagination, bulk ops +2. [Users Reference](references/users.md) - Endpoints, user types, scopes + +### I want to download recordings automatically +1. [Recording Pipeline](examples/recording-pipeline.md) - Webhook-triggered downloads +2. [Recordings Reference](references/recordings.md) - File types, download auth + +### I want to receive real-time events +1. [Webhook Server](examples/webhook-server.md) - CRC validation, signature check +2. Cross-reference: [zoom-webhooks](../webhooks/SKILL.md) for comprehensive webhook docs +3. Cross-reference: [zoom-websockets](../websockets/SKILL.md) for WebSocket events + +### I want to use GraphQL instead of REST +1. [GraphQL Queries](examples/graphql-queries.md) - Queries, mutations, pagination +2. [GraphQL Reference](references/graphql.md) - Available entities, scopes, rate limits + +### I want to set up authentication +1. [Authentication Flows](concepts/authentication-flows.md) - All OAuth methods +2. Cross-reference: [zoom-oauth](../oauth/SKILL.md) for full OAuth implementation + +### I'm hitting rate limits +1. [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) - Limits by plan, strategies +2. [Rate Limits Reference](references/rate-limits.md) - Detailed tables +3. [Common Issues](troubleshooting/common-issues.md) - Practical solutions + +### I'm getting errors +1. [Common Errors](troubleshooting/common-errors.md) - Error code tables +2. [Common Issues](troubleshooting/common-issues.md) - Diagnostic workflow + +### I want to build webinars +1. [Webinars Reference](references/webinars.md) - Endpoints, types, registrants +2. [Meeting Lifecycle](examples/meeting-lifecycle.md) - Similar patterns apply + +### I want to integrate Zoom Phone +1. [Phone Reference](references/phone.md) - Phone API endpoints +2. [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) - 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 `me` keyword 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](concepts/authentication-flows.md) + +2. **`me` keyword behaves differently by app type** + - User OAuth: MUST use `me` + - S2S OAuth: MUST NOT use `me` + - See: [API Architecture](concepts/api-architecture.md) + +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](concepts/rate-limiting-strategy.md) + +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](concepts/rate-limiting-strategy.md) + +5. **UUID double-encoding is required for certain UUIDs** + - UUIDs starting with `/` or containing `//` must be double-encoded + - See: [API Architecture](concepts/api-architecture.md) + +6. **Pagination: use `next_page_token`, not `page_number`** + - `page_number` is legacy and being phased out + - `next_page_token` is the recommended approach + - See: [Common Issues](troubleshooting/common-issues.md) + +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](examples/graphql-queries.md) + +--- + +## Quick Reference + +### "401 Unauthorized" +→ [Authentication Flows](concepts/authentication-flows.md) - Token expired or wrong scopes + +### "429 Too Many Requests" +→ [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) - Check headers for reset time + +### "Invalid token" when using userId +→ [API Architecture](concepts/api-architecture.md) - User OAuth apps must use `me` + +### "How do I paginate results?" +→ [Common Issues](troubleshooting/common-issues.md) - Use `next_page_token` + +### "Webhooks not arriving" +→ [Webhook Server](examples/webhook-server.md) - CRC validation required + +### "Recording download fails" +→ [Recording Pipeline](examples/recording-pipeline.md) - Bearer auth + follow redirects + +### "How do I create a meeting?" +→ [Meeting Lifecycle](examples/meeting-lifecycle.md) - 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](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/rest-api/concepts/api-architecture.md b/partner-built/zoom-plugin/skills/rest-api/concepts/api-architecture.md new file mode 100644 index 00000000..987ce39d --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/concepts/api-architecture.md @@ -0,0 +1,307 @@ +# 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/graphql +``` + +## Regional Base URLs + +The OAuth token response includes an `api_url` field indicating the user's data region. Use this for data residency compliance: + +```json +{ + "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 + +```javascript +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 + +```bash +# User OAuth app — MUST use me +GET /v2/users/me +GET /v2/users/me/meetings + +# S2S OAuth app — MUST use actual userId or email +GET /v2/users/abc123def +GET /v2/users/john@example.com +GET /v2/users/john@example.com/meetings +``` + +### Common Error + +Using `userId` with a User-level OAuth token: + +```json +{ + "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**: + +```javascript +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 + +```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 + +```json +{ + "topic": "Team Meeting", + "type": 2, + "start_time": "2025-03-15T10:00:00", + "timezone": "America/Los_Angeles", + "duration": 60 +} +``` + +Or using UTC directly: + +```json +{ + "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: + +```bash +GET /v2/users/me/recordings?from=2025-01-01&to=2025-01-31 +``` + +## Download 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): +```bash +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): +```bash +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: + +```javascript +// Node.js — fetch follows redirects by default +const response = await fetch(downloadUrl, { + headers: { 'Authorization': `Bearer ${accessToken}` }, + redirect: 'follow' +}); + +const fileBuffer = await response.arrayBuffer(); +``` + +```python +# 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:** + +```json +{ + "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](https://support.zoom.us/hc/en-us/articles/4413265586189) 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](authentication-flows.md) 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/ diff --git a/partner-built/zoom-plugin/skills/rest-api/concepts/authentication-flows.md b/partner-built/zoom-plugin/skills/rest-api/concepts/authentication-flows.md new file mode 100644 index 00000000..bc40cd84 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/concepts/authentication-flows.md @@ -0,0 +1,295 @@ +# 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](https://marketplace.zoom.us/) → **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 + +```bash +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 + +```json +{ + "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 + +```javascript +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 + +```python +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 None +``` + +## User 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 expired +``` + +### Exchange Code for Token + +```bash +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 + +```bash +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: + +```bash +# CORRECT for user OAuth +GET /v2/users/me/meetings + +# WRONG for user OAuth — will return "Invalid access token" +GET /v2/users/abc123/meetings +``` + +## Common 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 + +```javascript +// 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 CREDENTIALS +``` + +## Error 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 | + +```javascript +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)** diff --git a/partner-built/zoom-plugin/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md b/partner-built/zoom-plugin/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md new file mode 100644 index 00000000..9d3c38a5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md @@ -0,0 +1,38 @@ +--- +title: "Meeting URLs vs Meeting SDK Joining" +--- + +# 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_url` with 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 links +- `start_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 + diff --git a/partner-built/zoom-plugin/skills/rest-api/concepts/rate-limiting-strategy.md b/partner-built/zoom-plugin/skills/rest-api/concepts/rate-limiting-strategy.md new file mode 100644 index 00000000..18477fd3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/concepts/rate-limiting-strategy.md @@ -0,0 +1,347 @@ +# 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:** +```json +{ + "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: 55 +``` + +### Example — 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: 1705312800 +``` + +### Example — 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:00Z +``` + +## Strategy 1: Exponential Backoff with Jitter + +The simplest retry strategy for handling 429 responses: + +```javascript +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: + +```javascript +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: + +```javascript +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 + +```javascript +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 + +```javascript +// 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 + +```javascript +// 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 + +```javascript +// 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](../references/rate-limits.md) diff --git a/partner-built/zoom-plugin/skills/rest-api/examples/graphql-queries.md b/partner-built/zoom-plugin/skills/rest-api/examples/graphql-queries.md new file mode 100644 index 00000000..c1194659 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/examples/graphql-queries.md @@ -0,0 +1,14 @@ +# 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. + diff --git a/partner-built/zoom-plugin/skills/rest-api/examples/meeting-lifecycle.md b/partner-built/zoom-plugin/skills/rest-api/examples/meeting-lifecycle.md new file mode 100644 index 00000000..93946e15 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/examples/meeting-lifecycle.md @@ -0,0 +1,599 @@ +# 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](../concepts/authentication-flows.md)) +- Scopes: `meeting:write`, `meeting:read` (minimum) +- Base URL: `https://api.zoom.us/v2` + +## Step 1: Create a Meeting + +### Instant Meeting (No Fixed Time) + +```bash +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 + +```bash +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 + +```javascript +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 + +```json +{ + "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 + +```bash +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 + +```javascript +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: + +```javascript +// 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 + +```bash +curl "https://api.zoom.us/v2/meetings/93123456789" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js Example + +```javascript +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 + +```bash +curl "https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=30" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js with Pagination + +```javascript +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 + +```bash +curl -X DELETE "https://api.zoom.us/v2/meetings/93123456789" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js Example + +```javascript +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](webhook-server.md) 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` + +```json +{ + "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 + +```javascript +// 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 + +```javascript +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 + +```javascript +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 + +```javascript +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 + +```javascript +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). + +```javascript +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 + +```javascript +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, `me` keyword, 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 + +- [Create Meeting API](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate) +- [Update Meeting API](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate) +- [Meeting Events](https://developers.zoom.us/docs/api/meetings/events/) diff --git a/partner-built/zoom-plugin/skills/rest-api/examples/recording-pipeline.md b/partner-built/zoom-plugin/skills/rest-api/examples/recording-pipeline.md new file mode 100644 index 00000000..3c4e9348 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/examples/recording-pipeline.md @@ -0,0 +1,17 @@ +# 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_url` without attaching a bearer token. +- Not handling redirect responses from `download_url`. +- Assuming recording is available immediately after meeting ends (processing delays). + diff --git a/partner-built/zoom-plugin/skills/rest-api/examples/user-management.md b/partner-built/zoom-plugin/skills/rest-api/examples/user-management.md new file mode 100644 index 00000000..aaba5cd5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/examples/user-management.md @@ -0,0 +1,17 @@ +# 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. + diff --git a/partner-built/zoom-plugin/skills/rest-api/examples/webhook-server.md b/partner-built/zoom-plugin/skills/rest-api/examples/webhook-server.md new file mode 100644 index 00000000..1af4f07f --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/examples/webhook-server.md @@ -0,0 +1,589 @@ +# 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 + +```bash +npm install express body-parser crypto +``` + +### 2. Basic Webhook Server + +```javascript +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 + +```javascript +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 + +```json +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "qgg8vlvZRS6UYooatFL8Aw" + }, + "event_ts": 1654503849680 +} +``` + +### CRC Response Example + +```json +{ + "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 + +```javascript +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 + +```http +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 + +```javascript +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 + +```javascript +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 + +```javascript +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 + +```javascript +// 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 + +```javascript +// 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 + +```bash +# .env +ZOOM_WEBHOOK_SECRET=your_webhook_secret_token_here +PORT=3000 +NODE_ENV=production +``` + +### Loading Environment Variables + +```javascript +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 + +```bash +# 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/webhook +``` + +## Testing + +### Test CRC Validation + +```bash +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: +```json +{ + "plainToken": "test_token_123", + "encryptedToken": "..." +} +``` + +### Test Event Handling + +```bash +# 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](../../webhooks/SKILL.md) + +## 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 + +- [Using Webhooks](https://developers.zoom.us/docs/api/webhooks/) +- [Webhook Sample App](https://github.com/zoom/webhook-sample-node.js) +- [Event Reference](https://developers.zoom.us/docs/api/rest/webhook-reference/) diff --git a/partner-built/zoom-plugin/skills/rest-api/references/accounts.md b/partner-built/zoom-plugin/skills/rest-api/references/accounts.md new file mode 100644 index 00000000..3ee47bc1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/accounts.md @@ -0,0 +1,125 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/ai-companion.md b/partner-built/zoom-plugin/skills/rest-api/references/ai-companion.md new file mode 100644 index 00000000..f73e41e5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/ai-companion.md @@ -0,0 +1,37 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/ai-services.md b/partner-built/zoom-plugin/skills/rest-api/references/ai-services.md new file mode 100644 index 00000000..b25e5c8d --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/ai-services.md @@ -0,0 +1,43 @@ +# 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](../../scribe/SKILL.md) +- Authentication details: [authentication.md](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 `scribe` skill 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/authentication.md b/partner-built/zoom-plugin/skills/rest-api/references/authentication.md new file mode 100644 index 00000000..4d1add9a --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/authentication.md @@ -0,0 +1,438 @@ +# 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](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select **Server-to-Server OAuth** +4. Note your credentials: + - Account ID + - Client ID + - Client Secret + +### Get Access Token + +```bash +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 + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Code Example (Node.js) + +```javascript +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 expired +``` + +### Step 1: Create OAuth App + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +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=STATE +``` + +### Step 4: Exchange Code for Token + +```bash +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 + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "refresh_token": "eyJhbGciOiJIUzI1NiJ9...", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Step 5: Refresh Token + +```bash +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) + +```javascript +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 + +```javascript +// 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); // BAD +``` + +## Error 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 + +```javascript +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: + +```javascript +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 + +```javascript +// 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/ diff --git a/partner-built/zoom-plugin/skills/rest-api/references/auto-dialer.md b/partner-built/zoom-plugin/skills/rest-api/references/auto-dialer.md new file mode 100644 index 00000000..b0c62a84 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/auto-dialer.md @@ -0,0 +1,62 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/calendar.md b/partner-built/zoom-plugin/skills/rest-api/references/calendar.md new file mode 100644 index 00000000..1ba41c18 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/calendar.md @@ -0,0 +1,100 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/chatbot.md b/partner-built/zoom-plugin/skills/rest-api/references/chatbot.md new file mode 100644 index 00000000..5c162641 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/chatbot.md @@ -0,0 +1,40 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/clips.md b/partner-built/zoom-plugin/skills/rest-api/references/clips.md new file mode 100644 index 00000000..aca587e2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/clips.md @@ -0,0 +1,85 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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.` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/cobrowse-sdk-api.md b/partner-built/zoom-plugin/skills/rest-api/references/cobrowse-sdk-api.md new file mode 100644 index 00000000..48dfa08f --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/cobrowse-sdk-api.md @@ -0,0 +1,40 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/commerce.md b/partner-built/zoom-plugin/skills/rest-api/references/commerce.md new file mode 100644 index 00000000..e310a5a3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/commerce.md @@ -0,0 +1,111 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/contact-center.md b/partner-built/zoom-plugin/skills/rest-api/references/contact-center.md new file mode 100644 index 00000000..8199dd7e --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/contact-center.md @@ -0,0 +1,458 @@ +# Zoom Contact Center API + +Authoritative endpoint inventory for Contact Center. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/contact-center/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 278 | +| Path templates | 157 | +| Tags | 25 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Address Books | 21 | +| Agent Statuses | 5 | +| Asset Library | 12 | +| Call Control | 3 | +| Campaigns | 21 | +| Dispositions | 10 | +| Engagements | 8 | +| Flows | 11 | +| Inboxes | 18 | +| Logs | 5 | +| Messaging | 1 | +| Notes | 4 | +| Operating Hours | 14 | +| Queues | 35 | +| Recordings | 4 | +| Regions | 7 | +| Reports V2(CX Analytics) | 11 | +| Reports(Legacy Reports) | 7 | +| Roles | 10 | +| Routing Profiles | 10 | +| Skills | 11 | +| SMS | 1 | +| Teams | 14 | +| Users | 22 | +| Variables | 13 | + +## Endpoints by Tag + +### Address Books + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/address_books` | List address books | `listAddressBooks` | +| POST | `/contact_center/address_books` | Create an address book | `createAddressBook` | +| GET | `/contact_center/address_books/contacts/{contactId}/custom_fields` | List a contact's custom fields | `ListContactCustomFields` | +| GET | `/contact_center/address_books/custom_fields` | List an address book's custom fields | `Listaddressbookcustomfields` | +| POST | `/contact_center/address_books/custom_fields` | Create an address book custom field | `Createacustomfield` | +| DELETE | `/contact_center/address_books/custom_fields/{customFieldId}` | Delete an address book custom field | `Deleteancustomfield` | +| GET | `/contact_center/address_books/custom_fields/{customFieldId}` | Get an address book's custom field | `Getaaddressbookcustomfield` | +| PATCH | `/contact_center/address_books/custom_fields/{customFieldId}` | Update an address book custom field | `Updateacustomfield` | +| GET | `/contact_center/address_books/units` | List address book units | `listUnits` | +| POST | `/contact_center/address_books/units` | Create an address book unit | `createUnit` | +| DELETE | `/contact_center/address_books/units/{unitId}` | Delete an address book unit | `deleteUnit` | +| GET | `/contact_center/address_books/units/{unitId}` | Get an address book unit | `getUnit` | +| PATCH | `/contact_center/address_books/units/{unitId}` | Update an address book unit | `updateUnit` | +| DELETE | `/contact_center/address_books/{addressBookId}` | Delete an address book | `deleteAddressBook` | +| GET | `/contact_center/address_books/{addressBookId}` | Get an address book | `getAddressBook` | +| PATCH | `/contact_center/address_books/{addressBookId}` | Update an address book | `updateAddressBook` | +| GET | `/contact_center/address_books/{addressBookId}/contacts` | List address book contacts | `listContacts` | +| POST | `/contact_center/address_books/{addressBookId}/contacts` | Create an address book contact | `createContact` | +| DELETE | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Delete an address book contact | `contactDelete` | +| GET | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Get an address book contact | `getContact` | +| PATCH | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Update an address book contact | `updateContact` | + +### Agent Statuses + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/system_statuses` | List system statuses | `listSystemStatus` | +| POST | `/contact_center/system_statuses` | Create a system status | `createSystemStatus` | +| DELETE | `/contact_center/system_statuses/{statusId}` | Delete a system status | `deleteSystemStatus` | +| GET | `/contact_center/system_statuses/{statusId}` | Get a system status | `getAStatus` | +| PATCH | `/contact_center/system_statuses/{statusId}` | Update a system status | `updateSystemStatus` | + +### Asset Library + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/asset_library/assets` | List assets | `listAssets` | +| POST | `/contact_center/asset_library/assets` | Create an asset | `createAnAsset` | +| DELETE | `/contact_center/asset_library/assets/{assetId}` | Delete an asset | `deleteAnAsset` | +| GET | `/contact_center/asset_library/assets/{assetId}` | Get an asset | `getAnAsset` | +| PATCH | `/contact_center/asset_library/assets/{assetId}` | Update an asset | `updateAnAsset` | +| POST | `/contact_center/asset_library/assets/{assetId}/duplicate` | Duplicate an asset | `duplicateAnAsset` | +| DELETE | `/contact_center/asset_library/assets/{assetId}/items` | Delete asset items | `Deleteassetitems` | +| GET | `/contact_center/asset_library/categories` | List asset categories | `listAssetCategories` | +| POST | `/contact_center/asset_library/categories` | Create an asset category | `createAnAssetCategory` | +| DELETE | `/contact_center/asset_library/categories/{categoryId}` | Delete an asset category | `deleteAnAssetCategory` | +| GET | `/contact_center/asset_library/categories/{categoryId}` | Get an asset category | `getAnAssetCategory` | +| PATCH | `/contact_center/asset_library/categories/{categoryId}` | Update an asset category | `updateAnAssetCategory` | + +### Call Control + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PUT | `/contact_center/engagements/{engagementId}/recording/{command}` | Control an engagement's recording | `engagementRecordingControl` | +| POST | `/contact_center/users/{userId}/commands` | Command control of a user | `userControl` | +| GET | `/contact_center/users/{userId}/devices` | List user's devices | `Listuserdevices` | + +### Campaigns + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/outbound_campaign/campaigns` | List outbound campaigns | `listOutboundCampaigns` | +| POST | `/contact_center/outbound_campaign/campaigns` | Create an outbound campaign | `createOutboundCampaign` | +| DELETE | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Delete an outbound campaign | `deleteOutboundCampaign` | +| GET | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Get an outbound campaign | `getOutboundCampaign` | +| PATCH | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Update an outbound campaign | `updateOutboundCampaign` | +| PATCH | `/contact_center/outbound_campaign/campaigns/{campaignId}/status` | Update an outbound campaign status | `Updateanoutboundcampaignstatus` | +| GET | `/contact_center/outbound_campaign/contact_lists` | List campaign contact lists | `listCampaignContactLists` | +| PATCH | `/contact_center/outbound_campaign/contact_lists` | Batch update campaign contact lists | `Batchupdatecampaigncontactlists` | +| POST | `/contact_center/outbound_campaign/contact_lists` | Create a campaign contact list | `createCampaignContactList` | +| DELETE | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Remove a campaign contact list | `deleteCampaignContactList` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Get a campaign contact list | `getCampaignContactList` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Update a campaign contact list | `updateCampaignContactList` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | List a campaign contact list's contacts | `listCampaignContactListContacts` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | Update a batch of contacts on a campaign contact list | `Updateabatchofcontactsonacampaigncontactlist` | +| POST | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | Create a campaign contact list's contact | `createCampaignContactListContact` | +| DELETE | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Remove campaign contact list's contact | `deleteCampaigncontactListContact` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Get a campaign contact list's contact | `getCampaignContactListContact` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Update contact on a campaign contact list | `updateCampaignContactListContact` | +| DELETE | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | Batch delete a campaign DNC list's phones | `batchDeleteCampaignDncListPhone` | +| GET | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | List campaign DNC phone numbers | `listCampaignDncListPhones` | +| POST | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | Batch create a campaign DNC list's phones | `batchCreateCampaignDncListPhones` | + +### Dispositions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/dispositions` | List dispositions | `listDispositions` | +| POST | `/contact_center/dispositions` | Create a disposition | `createDisposition` | +| GET | `/contact_center/dispositions/sets` | List disposition sets | `listSets` | +| POST | `/contact_center/dispositions/sets` | Create a disposition set | `createSet` | +| DELETE | `/contact_center/dispositions/sets/{dispositionSetId}` | Delete a disposition set | `deleteSet` | +| GET | `/contact_center/dispositions/sets/{dispositionSetId}` | Get a disposition set | `getSet` | +| PATCH | `/contact_center/dispositions/sets/{dispositionSetId}` | Update a disposition set | `updateSet` | +| DELETE | `/contact_center/dispositions/{dispositionId}` | Delete a disposition | `deleteDisposition` | +| GET | `/contact_center/dispositions/{dispositionId}` | Get a disposition | `getDisposition` | +| PATCH | `/contact_center/dispositions/{dispositionId}` | Update a disposition | `updateDisposition` | + +### Engagements + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/engagement` | Start an engagement | `Startworkitemengagement` | +| GET | `/contact_center/engagements` | List engagements | `listEngagements` | +| GET | `/contact_center/engagements/{engagementId}` | Get an engagement | `getEngagement` | +| PATCH | `/contact_center/engagements/{engagementId}` | Update an engagement | `updateEngagement` | +| GET | `/contact_center/engagements/{engagementId}/attachments` | Get an engagement's attachments | `ListAttachments` | +| GET | `/contact_center/engagements/{engagementId}/events` | Get an engagement's events | `getEngagementEvents` | +| GET | `/contact_center/engagements/{engagementId}/recordings/status` | Poll an engagement recording's status | `EngagementRecordingStatus` | +| GET | `/contact_center/engagements/{engagementId}/survey` | Get an engagement's survey | `getEngagementSurvey` | + +### Flows + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/flows` | List flows | `listFlows` | +| POST | `/contact_center/flows` | Import a flow | `ImportFlow` | +| DELETE | `/contact_center/flows/{flowId}` | Delete a flow | `DeleteFlow` | +| GET | `/contact_center/flows/{flowId}` | Get a flow | `getAFlow` | +| PATCH | `/contact_center/flows/{flowId}` | Edit a flow | `EditFlow` | +| DELETE | `/contact_center/flows/{flowId}/entry_points` | Remove flow entry points | `RemoveFlowEntryPoints` | +| GET | `/contact_center/flows/{flowId}/entry_points` | List flow's entry points | `ListFlowEntryPoints` | +| POST | `/contact_center/flows/{flowId}/entry_points` | Add flow entry points | `AddFlowEntryPoints` | +| GET | `/contact_center/flows/{flowId}/export` | Export a flow | `ExportFlow` | +| PUT | `/contact_center/flows/{flowId}/publish` | Publish a flow | `PublishFlow` | +| GET | `/contact_center/flows_entry_points` | List entry points | `ListentryPoints` | + +### Inboxes + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/inboxes` | Delete inboxes | `inboxesDelete` | +| GET | `/contact_center/inboxes` | List inboxes | `listInbox` | +| POST | `/contact_center/inboxes` | Create an inbox | `inboxCreate` | +| DELETE | `/contact_center/inboxes/messages` | Delete inbox messages | `inboxesMessagesDelete` | +| GET | `/contact_center/inboxes/messages` | List an account's inbox messages | `listInboxesMessages` | +| GET | `/contact_center/inboxes/{inboxId}` | Get an inbox | `getInbox` | +| PATCH | `/contact_center/inboxes/{inboxId}` | Update an inbox | `inboxUpdate` | +| PATCH | `/contact_center/inboxes/{inboxId}/email_notification` | Update an inbox email notification | `Updateaninboxemailnotification` | +| GET | `/contact_center/inboxes/{inboxId}/email_notifications` | Get inbox email notification list | `Getinboxemailnotificationlist` | +| DELETE | `/contact_center/inboxes/{inboxId}/messages` | Delete an inbox's messages | `inboxMessagesDelete` | +| GET | `/contact_center/inboxes/{inboxId}/messages` | List an inbox's messages | `listInboxMessages` | +| DELETE | `/contact_center/inboxes/{inboxId}/messages/{messageId}` | Delete an inbox message | `inboxMessageDelete` | +| DELETE | `/contact_center/inboxes/{inboxId}/queues` | Remove inbox access queues | `unassignInboxQueues` | +| GET | `/contact_center/inboxes/{inboxId}/queues` | Get inbox access queues | `listInboxQueues` | +| POST | `/contact_center/inboxes/{inboxId}/queues` | Assign inbox access queues | `assignInboxQueues` | +| DELETE | `/contact_center/inboxes/{inboxId}/users` | Unassign inbox access users | `unassignInboxUsers` | +| GET | `/contact_center/inboxes/{inboxId}/users` | Get an inbox's users | `listInboxUsers` | +| POST | `/contact_center/inboxes/{inboxId}/users` | Assign inbox access users | `assignInboxUsers` | + +### Logs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/email/messages` | List email message history | `getEmailMessageHistory` | +| GET | `/contact_center/messaging/messages` | List message history | `getMessageHistory` | +| GET | `/contact_center/sms` | List SMS logs | `listSMS` | +| GET | `/contact_center/voice_calls` | List voice call logs | `listVoiceCall` | +| GET | `/contact_center/work_item/messages` | List work item message history | `getWorkItemMessageHistory` | + +### Messaging + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/messages` | Send a message | `SendaMessage` | + +### Notes + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/engagements/notes` | List notes | `notes` | +| GET | `/contact_center/engagements/{engagementId}/notes` | List engagement notes | `engagementNotes` | +| GET | `/contact_center/engagements/{engagementId}/notes/{noteId}` | Get a note | `getNote` | +| PATCH | `/contact_center/engagements/{engagementId}/notes/{noteId}` | Update a note | `noteUpdate` | + +### Operating Hours + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/business_hours` | List business hours | `listBusinessHours` | +| POST | `/contact_center/business_hours` | Create business hours | `businessHourCreate` | +| DELETE | `/contact_center/business_hours/{businessHourId}` | Delete business hours | `businessHourDelete` | +| GET | `/contact_center/business_hours/{businessHourId}` | Get business hours | `getABusinessHour` | +| PATCH | `/contact_center/business_hours/{businessHourId}` | Update business hours | `businessHourUpdate` | +| GET | `/contact_center/business_hours/{businessHourId}/flows` | List the business hours' flows | `listBusinessHourFlows` | +| GET | `/contact_center/business_hours/{businessHourId}/queues` | List the business hours' queues | `listBusinessHourQueues` | +| GET | `/contact_center/closures` | List closures | `listClosures` | +| POST | `/contact_center/closures` | Create a closure set | `closuresSetCreate` | +| DELETE | `/contact_center/closures/{closureSetId}` | Delete a closure set | `closureSetDelete` | +| GET | `/contact_center/closures/{closureSetId}` | Get a closure set | `getAClosureSet` | +| PATCH | `/contact_center/closures/{closureSetId}` | Update a closure set | `closureSetUpdate` | +| GET | `/contact_center/closures/{closureSetId}/flows` | List the closures' flows | `listClosureSetFlows` | +| GET | `/contact_center/closures/{closureSetId}/queues` | List the closures' queues | `listClosureSetQueues` | + +### Queues + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/queue_templates` | List queue templates | `Listqueuetemplates` | +| GET | `/contact_center/queue_templates/{queueTemplateId}` | Get a queue template | `getQueueTemplate` | +| GET | `/contact_center/queues` | List queues | `listQueues` | +| POST | `/contact_center/queues` | Create a queue | `queueCreate` | +| DELETE | `/contact_center/queues/batch` | Batch delete queues | `Batchdeletequeues` | +| POST | `/contact_center/queues/batch` | Batch create queues with a template | `Batchcreatequeueswithatemplate` | +| DELETE | `/contact_center/queues/{queueId}` | Delete a queue | `queueDelete` | +| GET | `/contact_center/queues/{queueId}` | Get a queue | `getAQueue` | +| PATCH | `/contact_center/queues/{queueId}` | Update a queue | `queueUpdate` | +| GET | `/contact_center/queues/{queueId}/agents` | List queue agents | `getQueueAgents` | +| POST | `/contact_center/queues/{queueId}/agents` | Assign queue agents | `assignQueueAgents` | +| DELETE | `/contact_center/queues/{queueId}/agents/{userId}` | Unassign a queue agent | `deleteQueueAgent` | +| PATCH | `/contact_center/queues/{queueId}/agents/{userId}` | Update a queue agent | `updateQueueAgent` | +| GET | `/contact_center/queues/{queueId}/dispositions` | List queue dispositions | `getQueueDispositions` | +| POST | `/contact_center/queues/{queueId}/dispositions` | Assign queue dispositions | `assignQueueDispositions` | +| GET | `/contact_center/queues/{queueId}/dispositions/sets` | List queue disposition sets | `getQueueDispositionSets` | +| POST | `/contact_center/queues/{queueId}/dispositions/sets` | Assign queue disposition sets | `assignQueueDispositionSets` | +| DELETE | `/contact_center/queues/{queueId}/dispositions/sets/{dispositionSetId}` | Unassign a queue disposition set | `deleteQueueDispositionSet` | +| DELETE | `/contact_center/queues/{queueId}/dispositions/{dispositionId}` | Unassign a queue disposition | `deleteQueueDisposition` | +| PATCH | `/contact_center/queues/{queueId}/interrupt` | Update a queue's interrupt settings | `updateQueueInterrupts` | +| DELETE | `/contact_center/queues/{queueId}/interrupt_menu` | Delete a queue's interrupt menu configuration | `deleteQueueInterruptMenu` | +| POST | `/contact_center/queues/{queueId}/interrupt_menu` | Assign a queue menu based interrupt | `assignQueueMenuBasedInterrupt` | +| GET | `/contact_center/queues/{queueId}/operating_hours` | Get a queue's operating hours | `getAQueueOperatingHours` | +| PATCH | `/contact_center/queues/{queueId}/operating_hours` | Update a queue's operating hours | `QueueOperatingHoursUpdate` | +| DELETE | `/contact_center/queues/{queueId}/recordings` | Delete queue recordings | `deleteQueueRecordings` | +| GET | `/contact_center/queues/{queueId}/recordings` | List queue recordings | `listQueueRecordings` | +| DELETE | `/contact_center/queues/{queueId}/scheduled_callbacks/attendees/{attendeeId}` | Delete an attendee from a scheduled callback event | `Deleteascheduledcallbackforanattendee` | +| POST | `/contact_center/queues/{queueId}/scheduled_callbacks/events` | Schedule a callback on a queue | `Scheduleacallbackonaqueue` | +| GET | `/contact_center/queues/{queueId}/scheduled_callbacks/supportive_slots` | List a queue's scheduled callbacks availability | `Listqueuescheduledcallbacksavailability` | +| GET | `/contact_center/queues/{queueId}/supervisors` | List queue supervisors | `getQueueSupervisors` | +| POST | `/contact_center/queues/{queueId}/supervisors` | Assign queue supervisors | `assignQueueSupervisors` | +| DELETE | `/contact_center/queues/{queueId}/supervisors/{userId}` | Unassign a queue supervisor | `deleteQueueSupervisor` | +| DELETE | `/contact_center/queues/{queueId}/teams` | Unassign multiple teams in a queue | `batchDeleteQueueTeams` | +| POST | `/contact_center/queues/{queueId}/teams` | Assign queue teams | `assignQueueTeams` | +| DELETE | `/contact_center/queues/{queueId}/teams/{teamId}` | Unassign a queue team | `deleteQueueTeam` | + +### Recordings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/engagements/{engagementId}/recordings` | Delete engagement recordings | `deleteEngagementRecordings` | +| GET | `/contact_center/engagements/{engagementId}/recordings` | List engagement recordings | `listEngagementRecordings` | +| GET | `/contact_center/recordings` | List recordings | `listRecordings` | +| DELETE | `/contact_center/recordings/{recordingId}` | Delete a recording | `deleteRecording` | + +### Regions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/regions` | List regions | `ListRegions` | +| POST | `/contact_center/regions` | Create a region | `CreateARegion` | +| DELETE | `/contact_center/regions/{regionId}` | Delete a region | `DeleteARegion` | +| GET | `/contact_center/regions/{regionId}` | Get a region | `GetARegion` | +| PATCH | `/contact_center/regions/{regionId}` | Update a region | `UpdateARegion` | +| GET | `/contact_center/regions/{regionId}/users` | List a region's users | `ListRegion'sUsers` | +| POST | `/contact_center/regions/{regionId}/users` | Assign users to a region | `AssignUsersToARegion` | + +### Reports V2(CX Analytics) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/analytics/dataset/historical/agent_performance` | List historical agent performance dataset data | `Listhistoricalagentperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/agent_timecard` | List historical agent timecard dataset data | `Listhistoricalagenttimecarddatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/disposition` | List historical disposition dataset data | `Listhistoricaldispositiondatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/engagement` | List historical engagement dataset data | `Listengagementdatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/expert_assist` | List historical expert assist dataset data | `Listexpertassistdatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/flow_performance` | List historical flow performance dataset data | `Listhistoricalflowperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/outbound_dialer_performance` | List historical outbound dialer performance dataset data | `Listhistoricaloutbounddialerperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/queue_performance` | List historical queue performance dataset data | `Listhistoricalqueueperformancedatasetdata` | +| GET | `/contact_center/analytics/log/historical/engagement` | List historical engagement log data | `Listhistoricalengagementlogs` | +| GET | `/contact_center/analytics/log/historical/journey` | List historical Zoom Phone to Contact Center call journey data | `ListhistoricalZoomphonetozcccalljourneydata` | +| GET | `/contact_center/reports/operation_logs` | List operation logs | `listOperationLogs` | + +### Reports(Legacy Reports) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/analytics/agents/leg_metrics` | List agent leg reports | `listAgentLegMetric` | +| GET | `/contact_center/analytics/agents/status_history` | List agent's status history reports | `listAgentStatusHistory` | +| GET | `/contact_center/analytics/agents/time_sheets` | List agent's time sheet reports | `listAgentTimeSheet` | +| GET | `/contact_center/analytics/historical/details/metrics` | List historical detail reports | `listHistoricalDetailMetric` | +| GET | `/contact_center/analytics/historical/queues/agents/metrics` | List historical agent reports by queue | `listQueueAgentsMetrics` | +| GET | `/contact_center/analytics/historical/queues/metrics` | List historical queue reports | `listHistoricalQueueMetric` | +| GET | `/contact_center/analytics/historical/queues/{queueId}/agents/metrics` | List historical queue's agents reports | `listQueueAgentMetric` | + +### Roles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/roles` | List roles | `listRoles` | +| POST | `/contact_center/roles` | Create a role | `createRole` | +| POST | `/contact_center/roles/duplicate` | Duplicate a role | `Duplicatearole` | +| DELETE | `/contact_center/roles/{roleId}` | Delete a role | `deleteRole` | +| GET | `/contact_center/roles/{roleId}` | Get a role | `getRole` | +| PATCH | `/contact_center/roles/{roleId}` | Update a role | `updateRole` | +| DELETE | `/contact_center/roles/{roleId}/privileges` | Delete role privileges | `Deleteroleprivileges` | +| GET | `/contact_center/roles/{roleId}/users` | List users of a role | `getRoleUsers` | +| POST | `/contact_center/roles/{roleId}/users` | Assign a role | `assignRoleUsers` | +| DELETE | `/contact_center/roles/{roleId}/users/{userId}` | Unassign a role | `deleteRoleUser` | + +### Routing Profiles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/agent_routing_profiles` | List agent routing profiles | `Listagentroutingprofiles` | +| POST | `/contact_center/agent_routing_profiles` | Create an agent routing profile | `Createanagentroutingprofile` | +| DELETE | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Delete an agent routing profile | `Deleteanagentroutingprofile` | +| GET | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Get an agent routing profile | `getAgentRoutingProfile` | +| PATCH | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Update an agent routing profile's details | `Updateanagentroutingprofile'sdetails` | +| GET | `/contact_center/consumer_routing_profiles` | List consumer routing profiles | `Listconsumerroutingprofiles` | +| POST | `/contact_center/consumer_routing_profiles` | Create a consumer routing profile | `Createaconsumerroutingprofile` | +| DELETE | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Delete a consumer routing profile | `Deleteaconsumerroutingprofile` | +| GET | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Get a consumer routing profile | `Getaconsumerroutingprofile` | +| PATCH | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Update a consumer routing profile's details | `Updateaconsumerroutingprofile'sdetails` | + +### Skills + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/skills` | List skills | `listSkills` | +| POST | `/contact_center/skills` | Create a skill | `skillCreate` | +| GET | `/contact_center/skills/categories` | List skill categories | `listSkillCategory` | +| POST | `/contact_center/skills/categories` | Create a skill category | `SkillCategoryCreate` | +| DELETE | `/contact_center/skills/categories/{skillCategoryId}` | Delete a skill category | `SkillCategoryDelete` | +| GET | `/contact_center/skills/categories/{skillCategoryId}` | Get a skill category | `getSkillCategory` | +| PATCH | `/contact_center/skills/categories/{skillCategoryId}` | Update a skill category | `SkillCategoryUpdate` | +| DELETE | `/contact_center/skills/{skillId}` | Delete a skill | `skillDelete` | +| GET | `/contact_center/skills/{skillId}` | Get a skill | `getSkill` | +| PATCH | `/contact_center/skills/{skillId}` | Update a skill | `skillNameUpdate` | +| GET | `/contact_center/skills/{skillId}/users` | List users of a skill | `listSkillUsers` | + +### SMS + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/sms` | Send an SMS | `contactCenterSMS` | + +### Teams + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/teams` | List teams | `listTeams` | +| POST | `/contact_center/teams` | Create a team | `CreateTeam` | +| DELETE | `/contact_center/teams/{teamId}` | Delete a team | `deleteTeam` | +| GET | `/contact_center/teams/{teamId}` | Get a team | `getTeamDetail` | +| PATCH | `/contact_center/teams/{teamId}` | Update a team | `Updateateam` | +| DELETE | `/contact_center/teams/{teamId}/agents` | Unassign team agents | `unassignTeamAgents` | +| GET | `/contact_center/teams/{teamId}/agents` | List team agents | `listTeamAgents` | +| POST | `/contact_center/teams/{teamId}/agents` | Assign team agents | `assignTeamAgents` | +| GET | `/contact_center/teams/{teamId}/children` | List a team's child teams | `getTeamChildTeams` | +| PATCH | `/contact_center/teams/{teamId}/move` | Move a team | `moveTeam` | +| GET | `/contact_center/teams/{teamId}/parents` | List team's parent teams | `getTeamParentTeams` | +| DELETE | `/contact_center/teams/{teamId}/supervisors` | Unassign team supervisors | `unassignTeamSupervisors` | +| GET | `/contact_center/teams/{teamId}/supervisors` | List team supervisors | `listTeamSupervisors` | +| POST | `/contact_center/teams/{teamId}/supervisors` | Assign team supervisors | `assignTeamSupervisors` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/users` | Batch delete user profiles | `batchDeleteUsers` | +| GET | `/contact_center/users` | List users' profiles | `users` | +| PATCH | `/contact_center/users` | Batch update user profiles | `BatchUpdateUsers` | +| POST | `/contact_center/users` | Create a user's profile | `createUser` | +| POST | `/contact_center/users/batch` | Batch create user profiles | `BatchCreateUsers` | +| PATCH | `/contact_center/users/status` | Batch update user status | `Batchupdateuserstatus` | +| GET | `/contact_center/users/templates` | List user templates | `ListUserTemplates` | +| POST | `/contact_center/users/templates` | Create a user template | `createAUserTemplate` | +| DELETE | `/contact_center/users/templates/{templateId}` | Delete a user template | `deleteAUserTemplate` | +| GET | `/contact_center/users/templates/{templateId}` | Get a user template | `Getanusertemplate` | +| PATCH | `/contact_center/users/templates/{templateId}` | Update a user template | `updateAUserTemplate` | +| DELETE | `/contact_center/users/{userId}` | Delete a user's profile | `userDelete` | +| GET | `/contact_center/users/{userId}` | Get a user's profile | `userGet` | +| PATCH | `/contact_center/users/{userId}` | Update a user's profile | `userUpdate` | +| PATCH | `/contact_center/users/{userId}/opt_in_out_queues` | Batch opt in or opt out a user's queues | `BatchOptInOrOutUserQueues` | +| GET | `/contact_center/users/{userId}/queues` | List user's queues | `listUserQueues` | +| DELETE | `/contact_center/users/{userId}/recordings` | Delete a user's recordings | `deleteUserRecordings` | +| GET | `/contact_center/users/{userId}/recordings` | List a user's recordings | `listUserRecordings` | +| GET | `/contact_center/users/{userId}/skills` | List user's skills | `ListAUserSkills` | +| POST | `/contact_center/users/{userId}/skills` | Assign user's skills | `assignSkills` | +| DELETE | `/contact_center/users/{userId}/skills/{skillId}` | Unassign user's skill | `deleteASkill` | +| PATCH | `/contact_center/users/{userId}/status` | Update a user's status | `Updateauser'sstatus` | + +### Variables + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/variable_logs` | List variable logs | `listVariableLogs` | +| DELETE | `/contact_center/variable_logs/{variableLogId}` | Delete a variable log | `deleteVariableLog` | +| GET | `/contact_center/variable_logs/{variableLogId}` | Get a variable log | `getVariableLog` | +| GET | `/contact_center/variables` | List variables | `variables` | +| POST | `/contact_center/variables` | Create a variable | `createVariable` | +| GET | `/contact_center/variables/groups` | List variable groups | `listVariableGroups` | +| POST | `/contact_center/variables/groups` | Create a variable group | `createVariableGroup` | +| DELETE | `/contact_center/variables/groups/{variableGroupId}` | Delete a variable group | `DeleteGroup` | +| GET | `/contact_center/variables/groups/{variableGroupId}` | Get a variable group | `getAVariableGroup` | +| PATCH | `/contact_center/variables/groups/{variableGroupId}` | Update a variable group | `updateVariableGroup` | +| DELETE | `/contact_center/variables/{variableId}` | Delete a variable | `variableDelete` | +| GET | `/contact_center/variables/{variableId}` | Get a variable | `variableGet` | +| PATCH | `/contact_center/variables/{variableId}` | Update a variable | `variableUpdate` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/crc.md b/partner-built/zoom-plugin/skills/rest-api/references/crc.md new file mode 100644 index 00000000..7a2a8a69 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/crc.md @@ -0,0 +1,80 @@ +# Zoom Conference Room Connector API + +Authoritative endpoint inventory for Conference Room Connector. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/crc/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 20 | +| Path templates | 9 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Account | 2 | +| Api Connector | 7 | +| Cisco/Polycom Rooms | 5 | +| Participant | 1 | +| Room Template | 5 | + +## Endpoints by Tag + +### Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/managed_rooms/account_setting` | Get Cisco/Polycom Room Account Setting | `getCiscoPolycomRoomAccountSetting` | +| PATCH | `/crc/managed_rooms/account_setting` | Update Cisco/Polycom Room Account Setting | `UpdateCiscoPolycomRoomAccountSetting` | + +### Api Connector + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/api_connectors` | List API Connectors | `GetListAPIConnectors` | +| POST | `/crc/api_connectors` | Create an API Connector | `CreateAPIConnector` | +| DELETE | `/crc/api_connectors/{connectorId}` | Delete an API Connector | `DeleteAPIConnector` | +| GET | `/crc/api_connectors/{connectorId}` | Get an API Connector | `GetAPIConnector` | +| PATCH | `/crc/api_connectors/{connectorId}` | Update an API Connector | `UpdateAPIConnector` | +| GET | `/crc/api_connectors/{connectorId}/private_key` | Get an API Connector's private key | `GetanAPIConnector'sprivatekey` | +| PATCH | `/crc/api_connectors/{connectorId}/private_key` | Update an API Connector's private key | `UpdateAPIConnectorPrivateKey` | + +### Cisco/Polycom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/managed_rooms` | List Managed Rooms | `ListManagedRooms` | +| POST | `/crc/managed_rooms` | Create a Managed Room | `CreateaManagedRoom` | +| DELETE | `/crc/managed_rooms/{deviceId}` | Delete a managed room | `Deleteamanagedroom` | +| GET | `/crc/managed_rooms/{deviceId}` | Get a Managed Room | `GetaManagedRoom` | +| PATCH | `/crc/managed_rooms/{deviceId}` | Update a Managed Room | `UpdateaManagedRoom` | + +### Participant + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/participant_identifier_code` | Get participant identifier code | `get_participant_identifier_code` | + +### Room Template + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/room_templates` | List Room Templates | `ListRoomTemplates` | +| POST | `/crc/room_templates` | Create a Room Template | `CreateaRoomTemplate` | +| DELETE | `/crc/room_templates/{templateId}` | Delete a room template | `Deletearoomtemplate` | +| GET | `/crc/room_templates/{templateId}` | Get a Room Template | `GetaRoomTemplate` | +| PATCH | `/crc/room_templates/{templateId}` | Update a Room Template | `UpdateaRoomTemplate` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/environment-variables.md b/partner-built/zoom-plugin/skills/rest-api/references/environment-variables.md new file mode 100644 index 00000000..409282b1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/environment-variables.md @@ -0,0 +1,21 @@ +# 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_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +## Notes + +- Use `ZOOM_ACCOUNT_ID` for server-to-server service integrations. +- User-level integrations require authorization code flow and `ZOOM_REDIRECT_URI`. diff --git a/partner-built/zoom-plugin/skills/rest-api/references/events.md b/partner-built/zoom-plugin/skills/rest-api/references/events.md new file mode 100644 index 00000000..35d8850b --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/events.md @@ -0,0 +1,222 @@ +# Zoom Events API + +Authoritative endpoint inventory for Events. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/events/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 90 | +| Path templates | 52 | +| Tags | 17 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Attendee Actions | 4 | +| Co Editors | 2 | +| Emails | 2 | +| Event Access | 5 | +| Events | 6 | +| Exhibitors | 6 | +| Files | 3 | +| Hubs | 5 | +| Registrants | 2 | +| Reports | 7 | +| Sessions | 15 | +| Speakers | 5 | +| Ticket Types | 8 | +| Tickets | 5 | +| Video On-Demand | 9 | +| Video On-Demand Registrations | 4 | +| Videos | 2 | + +## Endpoints by Tag + +### Attendee Actions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/attendee_action` | List event attendee actions | `ListEventAttendeeActions` | +| PATCH | `/zoom_events/events/{eventId}/attendee_action` | Update event attendee actions | `UpdateEventAttendeeActions` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendee_action` | List session attendee actions | `ListSessionAttendeeActions` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendee_action` | Update session attendee actions | `UpdateSessionAttendeeActions` | + +### Co Editors + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/coeditors` | List coeditors | `getCoEditors` | +| PATCH | `/zoom_events/events/{eventId}/coeditors` | Add or remove event co-editors | `coeditoractions` | + +### Emails + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/email_types` | List event email types | `listEmailTypes` | +| GET | `/zoom_events/events/{eventId}/email_types/{emailTypeId}/send_status` | List event emails sent status | `listEmailSentStatuses` | + +### Event Access + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/access_links` | List event access links | `getEventAccessLinks` | +| POST | `/zoom_events/events/{eventId}/access_links` | Create event access link | `createEventAccessLink` | +| DELETE | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Delete event access link | `deleteEventAccessLink` | +| GET | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Get event access link | `GetEventAccessLink` | +| PATCH | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Update event access | `updateEventAccess` | + +### Events + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events` | List events | `getEvents` | +| POST | `/zoom_events/events` | Create an event | `createEvent` | +| DELETE | `/zoom_events/events/{eventId}` | Delete an event | `deleteEvent` | +| GET | `/zoom_events/events/{eventId}` | Get an event | `getEventInfo` | +| PATCH | `/zoom_events/events/{eventId}` | Update an event | `updateEvent` | +| POST | `/zoom_events/events/{eventId}/event_actions` | Event actions | `EventActions` | + +### Exhibitors + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/exhibitors` | List exhibitors | `getExhibitors` | +| POST | `/zoom_events/events/{eventId}/exhibitors` | Create an exhibitor | `createExhibitor` | +| DELETE | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Delete an exhibitor | `deleteExhibitor` | +| GET | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Get an exhibitor | `getExhibitorInfo` | +| PATCH | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Update exhibitor for an event | `updateExhibitor` | +| GET | `/zoom_events/events/{eventId}/sponsor_tiers` | List sponsor tiers | `ListSponsorTiers` | + +### Files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/zoom_events/files` | Upload events file | `uploadEventFile` | +| POST | `/zoom_events/files/multipart` | Upload events multipart files | `uploadMultipartEventFile` | +| POST | `/zoom_events/files/multipart/upload` | Initiate and complete the multipart file upload | `initiateAndCompleteAEventMultipartUpload.` | + +### Hubs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs` | List hubs | `getHubList` | +| GET | `/zoom_events/hubs/{hubId}/hosts` | List hub Hosts | `gethubhostList` | +| POST | `/zoom_events/hubs/{hubId}/hosts` | Creates a new hub host | `createHubHost` | +| DELETE | `/zoom_events/hubs/{hubId}/hosts/{hostUserId}` | Remove hub host | `deleteHubHost` | +| GET | `/zoom_events/hubs/{hubId}/videos` | List hub videos | `ListHubVideos` | + +### Registrants + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/registrants` | List registrants | `getRegistrants` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendees` | List session attendees | `getSessionAttendeeList` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/reports/chat_transcripts` | Get chat transcripts report | `ChatTranscriptsReport` | +| GET | `/zoom_events/events/{eventId}/reports/custom_reports/{customReportId}` | Get custom report | `getCustomEventReport` | +| GET | `/zoom_events/events/{eventId}/reports/event_attendance` | Get event attendance (Live or Lobby) report | `EventAttendanceReport` | +| GET | `/zoom_events/events/{eventId}/reports/sessions/{sessionId}/attendance` | Get session attendance report | `SessionAttendanceReport` | +| GET | `/zoom_events/events/{eventId}/reports/survey` | Get event survey report | `EventSurveyReportApi` | +| GET | `/zoom_events/events/{eventId}/reports/ticket_registration` | Get event registrations report | `EventRegistrationsReport` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/reports/registrations` | Get VOD channel registration report | `VodChannelReigistration` | + +### Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/sessions` | List sessions | `getEventSessionList` | +| POST | `/zoom_events/events/{eventId}/sessions` | Create a session | `createEventSession` | +| DELETE | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Delete a session | `deleteEventSession` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Get the session information | `getEventSessionInfo` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Update a session | `updateEventSession` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/interpreters` | List session interpreters | `getSessionInterpreterList` | +| PUT | `/zoom_events/events/{eventId}/sessions/{sessionId}/interpreters` | Create or update session interpreters | `updateSessionInterpreters` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/join_token` | Get ticket session join token by Event ID and Session ID | `getSessionJoinToken` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/livestream` | Get session livestream configuration | `getSessionLivestreamConfiguration` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}/livestream` | Update session livestream configuration | `UpdateSessionLivestreamConfiguration` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/polls` | List session polls | `getSessionPolls` | +| PUT | `/zoom_events/events/{eventId}/sessions/{sessionId}/polls` | Create or update session polls | `updateSessionPolls` | +| DELETE | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | Delete session reservations | `DeleteSessionReservations` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | List session reservations | `ListSessionReservations` | +| POST | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | Add session reservations | `AddSessionReservations` | + +### Speakers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/speakers` | List speakers | `getSpeakers` | +| POST | `/zoom_events/events/{eventId}/speakers` | Create a speaker | `createSpeaker` | +| DELETE | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Delete a speaker | `deleteSpeaker` | +| GET | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Get a speaker | `getSpeaker` | +| PATCH | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Update a speaker | `updateSpeaker` | + +### Ticket Types + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/questions` | List registration questions for an event | `getRegistrationQuestionsForEvent` | +| PUT | `/zoom_events/events/{eventId}/questions` | Update registration questions for an event | `updateRegistrationQuestionsForEvent` | +| GET | `/zoom_events/events/{eventId}/ticket_types` | List ticket types | `getEventTicketTypes` | +| POST | `/zoom_events/events/{eventId}/ticket_types` | Create an event ticket type | `createTicketType` | +| DELETE | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}` | Delete a ticket type | `deleteEventTicketType` | +| PATCH | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}` | Update ticket type for an event | `updateTicketType` | +| GET | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}/questions` | List registration questions for ticket type | `getRegistrationQuestionsForTicketType` | +| PUT | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}/questions` | Update registration questions for ticket type | `updateRegistrationQuestionsForTicketType` | + +### Tickets + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/tickets` | List tickets | `getTickets` | +| POST | `/zoom_events/events/{eventId}/tickets` | Create tickets | `createTickets` | +| DELETE | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Delete a ticket | `deleteTicket` | +| GET | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Get a ticket | `getTicketDetails` | +| PATCH | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Update ticket | `Updateticket` | + +### Video On-Demand + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs/{hubId}/vod_channels` | List channels | `getVODChannels` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels` | Create VOD channel | `createVodChannel` | +| DELETE | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Delete VOD Channel | `DeleteVODChannel` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Get VOD channel details | `getVODChannelDetail` | +| PATCH | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Update VOD channel | `UpdateVideoChannel` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/actions` | VOD channel actions | `vodChannelActions` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos` | List VOD channel videos | `ListVODChannelVideos` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos` | Add VOD channel videos | `AddVODChannelVideos` | +| DELETE | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos/{videoId}` | Delete VOD channel video | `DeleteVODChannelVideo` | + +### Video On-Demand Registrations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registration_questions` | Get VOD Registration Questions | `getRegistrationQuestionsForVODChannel` | +| PUT | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registration_questions` | update VOD channel registration questions | `updateRegistrationQuestionsForVODchannel` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registrations` | List VOD Registration | `ListVODRegistration` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registrations` | VOD channel registration | `VODTicketRegistration` | + +### Videos + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/videos/{videoId}/metadata` | Get metadata for a specific video | `getVideoMetadata` | +| PATCH | `/zoom_events/videos/{videoId}/metadata` | Update metadata for a specific video. | `updateVideoMetadata` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/graphql.md b/partner-built/zoom-plugin/skills/rest-api/references/graphql.md new file mode 100644 index 00000000..9bb8efb6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/graphql.md @@ -0,0 +1,484 @@ +# Zoom GraphQL API (Beta) + +Flexible data queries with GraphQL as an alternative to REST. + +## Overview + +Zoom GraphQL API provides a single endpoint for querying and mutating Zoom data. Unlike REST APIs with fixed endpoints, GraphQL allows you to request exactly the data you need. + +**Status**: Beta (requires signup) + +## Key URLs + +| Resource | URL | +|----------|-----| +| **Playground** | https://nws.zoom.us/graphql/playground | +| **Beta Signup** | https://beta.zoom.us/key/GRAPHQL | +| **Endpoint** | `https://api.zoom.us/graphql` | + +## GraphQL vs REST + +| Feature | REST API | GraphQL API | +|---------|----------|-------------| +| Endpoints | 600+ endpoints | Single endpoint | +| Data fetching | Fixed data per endpoint | Client specifies exact fields | +| Multiple resources | Multiple requests | Single request | +| Over-fetching | Common | Eliminated | +| Schema | Optional (OpenAPI) | Mandatory, strongly typed | +| Learning curve | Lower | Higher | + +## Authentication + +Same OAuth 2.0 as REST API: + +```javascript +const headers = { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' +}; +``` + +- Server-to-Server OAuth supported +- User-authorized OAuth supported +- Access tokens valid for 1 hour + +## Available Entities + +| Entity | Query | Mutation | +|--------|-------|----------| +| Users | ✅ | ✅ | +| Meetings | ✅ | Partial | +| Webinars | ✅ | Partial | +| Chat Channels | Partial | - | +| Cloud Recordings | ✅ | - | +| Dashboards | ✅ | - | +| Groups | Partial | - | +| Reports | Partial | - | + +**Note**: Not all REST endpoints are available in GraphQL yet (beta). + +## Query Examples + +### List Users + +```graphql +query ListUsers { + users(first: 100) { + edges { + node { + id + firstName + lastName + email + department + type + status + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Get User Details + +```graphql +query GetUser($userId: ID!) { + user(id: $userId) { + id + firstName + lastName + email + department + timezone + type + createdAt + lastLoginTime + } +} +``` + +Variables: +```json +{ + "userId": "user_abc123" +} +``` + +### List Meetings + +```graphql +query ListMeetings($userId: ID!) { + user(id: $userId) { + meetings(first: 50) { + edges { + node { + id + topic + type + startTime + duration + timezone + joinUrl + } + } + } + } +} +``` + +### Get Meeting with Participants + +```graphql +query GetMeetingDetails($meetingId: ID!) { + meeting(id: $meetingId) { + id + topic + type + startTime + duration + host { + id + firstName + lastName + email + } + participants { + edges { + node { + id + name + email + joinTime + leaveTime + } + } + } + } +} +``` + +### Get Recordings + +```graphql +query GetRecordings($userId: ID!, $from: DateTime!, $to: DateTime!) { + user(id: $userId) { + recordings(from: $from, to: $to, first: 50) { + edges { + node { + id + topic + startTime + duration + totalSize + recordingFiles { + id + fileType + fileSize + downloadUrl + } + } + } + } + } +} +``` + +## Mutation Examples + +### Create User + +```graphql +mutation CreateUser($input: UserCreateInput!) { + createUser(input: $input) { + id + email + firstName + lastName + type + } +} +``` + +Variables: +```json +{ + "input": { + "action": "CUST_CREATE", + "userInfo": { + "email": "newuser@example.com", + "firstName": "John", + "lastName": "Doe", + "type": "BASIC" + } + } +} +``` + +### Update User + +```graphql +mutation UpdateUser($userId: ID!, $input: UserUpdateInput!) { + updateUser(id: $userId, input: $input) { + id + firstName + lastName + department + } +} +``` + +Variables: +```json +{ + "userId": "user_abc123", + "input": { + "firstName": "Jonathan", + "department": "Engineering" + } +} +``` + +## Making Requests + +### JavaScript/Node.js + +```javascript +async function graphqlQuery(query, variables = {}) { + const response = await fetch('https://api.zoom.us/graphql', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: query, + variables: variables + }) + }); + + const result = await response.json(); + + if (result.errors) { + throw new Error(result.errors[0].message); + } + + return result.data; +} + +// Usage +const users = await graphqlQuery(` + query { + users(first: 10) { + edges { + node { + id + email + firstName + lastName + } + } + } + } +`); + +console.log(users.users.edges); +``` + +### Python + +```python +import requests + +def graphql_query(query, variables=None): + response = requests.post( + 'https://api.zoom.us/graphql', + headers={ + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + }, + json={ + 'query': query, + 'variables': variables or {} + } + ) + + result = response.json() + + if 'errors' in result: + raise Exception(result['errors'][0]['message']) + + return result['data'] + +# Usage +users = graphql_query(''' + query { + users(first: 10) { + edges { + node { + id + email + } + } + } + } +''') +``` + +### cURL + +```bash +curl -X POST https://api.zoom.us/graphql \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query { users(first: 10) { edges { node { id email } } } }" + }' +``` + +## Pagination + +GraphQL uses cursor-based pagination: + +```graphql +query PaginatedUsers($cursor: String) { + users(first: 100, after: $cursor) { + edges { + node { + id + email + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +Fetching all pages: + +```javascript +async function fetchAllUsers() { + let allUsers = []; + let cursor = null; + let hasNextPage = true; + + while (hasNextPage) { + const result = await graphqlQuery( + `query($cursor: String) { + users(first: 100, after: $cursor) { + edges { + node { id email firstName lastName } + } + pageInfo { + hasNextPage + endCursor + } + } + }`, + { cursor } + ); + + allUsers.push(...result.users.edges.map(e => e.node)); + hasNextPage = result.users.pageInfo.hasNextPage; + cursor = result.users.pageInfo.endCursor; + } + + return allUsers; +} +``` + +## Error Handling + +GraphQL always returns HTTP 200. Errors are in the response body: + +```json +{ + "data": null, + "errors": [ + { + "message": "User not found", + "locations": [{ "line": 2, "column": 3 }], + "path": ["user"], + "extensions": { + "code": "NOT_FOUND" + } + } + ] +} +``` + +Handle errors: + +```javascript +async function safeQuery(query, variables) { + const response = await fetch('https://api.zoom.us/graphql', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ query, variables }) + }); + + const result = await response.json(); + + if (result.errors && result.errors.length > 0) { + const error = result.errors[0]; + console.error(`GraphQL Error: ${error.message}`); + console.error(`Code: ${error.extensions?.code}`); + throw new Error(error.message); + } + + return result.data; +} +``` + +## When to Use GraphQL vs REST + +### Use GraphQL When: +- Fetching specific fields from complex nested data +- Making multiple related queries in one request +- Building mobile apps where bandwidth matters +- Working with interconnected user, meeting, webinar data +- Prototyping and exploring the API + +### Use REST When: +- Need access to all 600+ endpoints (GraphQL coverage incomplete) +- Building simple CRUD applications +- Require HTTP caching +- Team is unfamiliar with GraphQL +- Production stability is critical (GraphQL is beta) + +## Playground + +The GraphQL Playground at https://nws.zoom.us/graphql/playground provides: +- Interactive query editor +- Auto-complete and syntax highlighting +- Query history +- Embedded documentation +- Schema explorer + +**Note**: Requires beta access. + +## Limitations (Beta) + +| Limitation | Notes | +|------------|-------| +| Coverage | Not all REST endpoints available | +| Stability | Beta - features may change | +| Access | Requires explicit beta signup | +| Documentation | Less comprehensive than REST | + +## Resources + +- **Playground**: https://nws.zoom.us/graphql/playground +- **Beta Signup**: https://beta.zoom.us/key/GRAPHQL +- **Postman Collection**: https://www.postman.com/zoom-developer/zoom-public-workspace/collection/2ub5ygf/zoom-graphql-collection-beta +- **Blog Post**: https://dev.to/zoom/the-zoom-graphql-api-playground-your-new-favorite-development-tool-59n5 diff --git a/partner-built/zoom-plugin/skills/rest-api/references/healthcare.md b/partner-built/zoom-plugin/skills/rest-api/references/healthcare.md new file mode 100644 index 00000000..d97f9612 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/healthcare.md @@ -0,0 +1,39 @@ +# 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](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/mail.md b/partner-built/zoom-plugin/skills/rest-api/references/mail.md new file mode 100644 index 00000000..5a7e819d --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/mail.md @@ -0,0 +1,131 @@ +# Zoom Mail API + +Authoritative endpoint inventory for Mail. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/mail/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 41 | +| Path templates | 26 | +| Tags | 10 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Drafts | 6 | +| History | 1 | +| Labels | 6 | +| Mailbox | 1 | +| Messages | 10 | +| Messages.Attachments | 1 | +| Settings | 2 | +| Settings.Delegates | 4 | +| Settings.Filters | 4 | +| Threads | 6 | + +## Endpoints by Tag + +### Drafts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/drafts` | List emails from draft folder | `list_draft_emails` | +| POST | `/emails/mailboxes/{email}/drafts` | Create a new draft email | `create_draft_email` | +| POST | `/emails/mailboxes/{email}/drafts/send` | Send out a draft email | `send_draft_email` | +| DELETE | `/emails/mailboxes/{email}/drafts/{draftId}` | Delete an existing draft email | `delete_draft_email` | +| GET | `/emails/mailboxes/{email}/drafts/{draftId}` | Get the specified draft email | `get_draft_email` | +| PUT | `/emails/mailboxes/{email}/drafts/{draftId}` | Update the specified draft email | `update_draft_email` | + +### History + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/history` | List history of events for mailbox | `list_mailbox_history` | + +### Labels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/labels` | List labels in the mailbox | `list_labels_in_mailbox` | +| POST | `/emails/mailboxes/{email}/labels` | Create a new label in mailbox | `create_label_in_mailbox` | +| DELETE | `/emails/mailboxes/{email}/labels/{labelId}` | Delete an existing label from mailbox | `delete_label_from_mailbox` | +| GET | `/emails/mailboxes/{email}/labels/{labelId}` | Get the specified label in mailbox | `get_label_in_mailbox` | +| PATCH | `/emails/mailboxes/{email}/labels/{labelId}` | Patch the specified label in mailbox | `patch_label_in_mailbox` | +| PUT | `/emails/mailboxes/{email}/labels/{labelId}` | Update the specified label in mailbox | `update_label_in_mailbox` | + +### Mailbox + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/profile` | Get the mailbox profile | `get_mailbox_profile` | + +### Messages + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/messages` | List emails from the mailbox | `list_emails` | +| POST | `/emails/mailboxes/{email}/messages` | Create a new email | `create_email` | +| POST | `/emails/mailboxes/{email}/messages/batchDelete` | Batch delete the specified emails | `batch_delete_emails` | +| POST | `/emails/mailboxes/{email}/messages/batchModify` | Batch modify the specified emails | `batch_modify_emails` | +| POST | `/emails/mailboxes/{email}/messages/send` | Send out an email | `send_email` | +| DELETE | `/emails/mailboxes/{email}/messages/{messageId}` | Delete an existing email | `delete_email` | +| GET | `/emails/mailboxes/{email}/messages/{messageId}` | Get the specified email | `get_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/modify` | Update the specified email | `update_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/trash` | Move the specified email to TRASH folder | `trash_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/untrash` | Move the specified email out of TRASH folder | `untrash_email` | + +### Messages.Attachments + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/messages/{messageId}/attachments/{attachmentId}` | Get the specified attachment for an email | `get_email_attachment` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/vacation` | Get mailbox vacation response setting | `get_mail_vacation_response_setting` | +| PUT | `/emails/mailboxes/{email}/settings/vacation` | Update mailbox vacation response setting | `update_mailbox_vacation_response_setting` | + +### Settings.Delegates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/delegates` | List delegates on the mailbox | `list_mailbox_delegates` | +| POST | `/emails/mailboxes/{email}/settings/delegates` | Grant a new delegate access on the mailbox | `grant_mailbox_delegate` | +| DELETE | `/emails/mailboxes/{email}/settings/delegates/{delegateEmail}` | Revoke an existing delegate access from the mailbox | `revoke_mailbox_delegate` | +| GET | `/emails/mailboxes/{email}/settings/delegates/{delegateEmail}` | Get the specified delegate on the mailbox | `get_mailbox_delegate` | + +### Settings.Filters + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/filters` | List email filters | `list_email_filters` | +| POST | `/emails/mailboxes/{email}/settings/filters` | Create an email filter | `create_email_filter` | +| DELETE | `/emails/mailboxes/{email}/settings/filters/{filterId}` | Delete the specified email filter | `delete_email_filter` | +| GET | `/emails/mailboxes/{email}/settings/filters/{filterId}` | Get the specified email filter | `get_email_filter` | + +### Threads + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/threads` | List email threads from the mailbox | `list_email_threads` | +| DELETE | `/emails/mailboxes/{email}/threads/{threadId}` | Delete an existing email thread | `delete_email_thread` | +| GET | `/emails/mailboxes/{email}/threads/{threadId}` | Get the specified email thread | `get_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/modify` | Update the specified thread | `update_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/trash` | Move the specified thread to TRASH folder | `trash_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/untrash` | Move the specified thread out of TRASH folder | `untrash_email_thread` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/marketplace-apps.md b/partner-built/zoom-plugin/skills/rest-api/references/marketplace-apps.md new file mode 100644 index 00000000..21512150 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/marketplace-apps.md @@ -0,0 +1,75 @@ +# Zoom Marketplace API + +Authoritative endpoint inventory for Marketplace. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/marketplace/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 27 | +| Path templates | 19 | +| Tags | 3 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| App | 23 | +| Apps | 1 | +| Manifest | 3 | + +## Endpoints by Tag + +### App + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/app/notifications` | Send app notifications | `Sendappnotifications` | +| GET | `/marketplace/app/custom_fields` | Get user's custom field values | `getCustomFieldValues` | +| DELETE | `/marketplace/app/event_subscription` | Unsubscribe app event subscription | `unSubscribeEventSubscription` | +| GET | `/marketplace/app/event_subscription` | Get user or account event subscription | `getUserEventSubscriptions` | +| POST | `/marketplace/app/event_subscription` | Create an event subscription | `createEventSubscription` | +| DELETE | `/marketplace/app/event_subscription/{eventSubscriptionId}` | Delete an event subscription | `deleteEventSubscription` | +| PATCH | `/marketplace/app/event_subscription/{eventSubscriptionId}` | Subscribe an event subscription | `subscribeEventSubscription` | +| GET | `/marketplace/apps` | List apps | `ListApps` | +| POST | `/marketplace/apps` | Create apps | `CreateApps` | +| DELETE | `/marketplace/apps/{appId}` | Deletes an app | `deleteApp` | +| GET | `/marketplace/apps/{appId}` | Get information about an app | `getAppInfo` | +| GET | `/marketplace/apps/{appId}/api_call_logs` | Get API call logs | `Getapicalllogs` | +| POST | `/marketplace/apps/{appId}/deeplink` | Generate Zoom App Deeplink | `GenerateZoomAppDeeplink` | +| POST | `/marketplace/apps/{appId}/preApprove` | Update app pre approval setting | `Updateapppreapprovalsetting` | +| GET | `/marketplace/apps/{appId}/requests` | Get an app's user requests | `getAppUserRequests` | +| PATCH | `/marketplace/apps/{appId}/requests` | Update app's request status | `updateAppRequestStatus` | +| POST | `/marketplace/apps/{appId}/requests` | Add app allow requests for users | `AddAppAllowRequestsForUsers` | +| POST | `/marketplace/apps/{appId}/rotate_client_secret` | Rotate client secret | `RotateClientSecret` | +| GET | `/marketplace/apps/{appId}/webhook_logs` | Get webhook logs | `getWebhookLogs` | +| GET | `/marketplace/monetization/entitlements` | Get app user entitlements | `getAppUserEntitlementRequests` | +| GET | `/marketplace/users/{userId}/apps` | Get a user's app requests | `getUserAppRequests` | +| PATCH | `/marketplace/users/{userId}/apps/{appId}/subscription` | Enable or disable user app subscription | `Enable/Disableuserappsubscription` | +| GET | `/marketplace/users/{userId}/entitlements` | Get a user's entitlements | `getUserEntitlementRequests` | + +### Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/zoomapp/deeplink` | Generate an app deeplink | `generateAppDeeplink` | + +### Manifest + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/marketplace/apps/manifest/validate` | Validate an app manifest | `validatingManifest` | +| GET | `/marketplace/apps/{appId}/manifest` | Export an app manifest from an existing app | `getAppManifest` | +| PUT | `/marketplace/apps/{appId}/manifest` | Update an app by manifest | `updateAppByManifest` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/meetings.md b/partner-built/zoom-plugin/skills/rest-api/references/meetings.md new file mode 100644 index 00000000..d29279cb --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/meetings.md @@ -0,0 +1,327 @@ +# Zoom Meetings API + +Authoritative endpoint inventory for Meetings. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/meetings/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 183 | +| Path templates | 128 | +| Tags | 19 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Archiving | 6 | +| Cloud Recording | 17 | +| Devices | 13 | +| H323 Devices | 4 | +| In-Meeting Apps | 2 | +| In-Meeting Features | 5 | +| Invitation & Registration | 10 | +| Live streaming | 4 | +| Meetings | 13 | +| PAC | 1 | +| Polls | 7 | +| Reports | 23 | +| SIP Phone | 4 | +| Summaries | 3 | +| Surveys | 3 | +| Templates | 2 | +| Tracking Field | 5 | +| TSP | 8 | +| Webinars | 53 | + +## Endpoints by Tag + +### Archiving + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/archive_files` | List archived files | `listArchivedFiles` | +| GET | `/archive_files/statistics` | Get archived file statistics | `getArchivedFileStatistics` | +| PATCH | `/archive_files/{fileId}` | Update an archived file's auto-delete status | `updateArchivedFile` | +| GET | `/meetings/{meetingId}/jointoken/local_archiving` | Get a meeting's archive token for local archiving | `meetingLocalArchivingArchiveToken` | +| DELETE | `/past_meetings/{meetingUUID}/archive_files` | Delete a meeting's archived files | `deleteArchivedFiles` | +| GET | `/past_meetings/{meetingUUID}/archive_files` | Get a meeting's archived files | `getArchivedFiles` | + +### Cloud Recording + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/recordings` | Delete meeting or webinar recordings | `recordingDelete` | +| GET | `/meetings/{meetingId}/recordings` | Get meeting recordings | `recordingGet` | +| GET | `/meetings/{meetingId}/recordings/analytics_details` | Get a meeting or webinar recording's analytics details | `analytics_details` | +| GET | `/meetings/{meetingId}/recordings/analytics_summary` | Get a meeting or webinar recording's analytics summary | `analytics_summary` | +| GET | `/meetings/{meetingId}/recordings/registrants` | List recording registrants | `meetingRecordingRegistrants` | +| POST | `/meetings/{meetingId}/recordings/registrants` | Create a recording registrant | `meetingRecordingRegistrantCreate` | +| GET | `/meetings/{meetingId}/recordings/registrants/questions` | Get registration questions | `recordingRegistrantsQuestionsGet` | +| PATCH | `/meetings/{meetingId}/recordings/registrants/questions` | Update registration questions | `recordingRegistrantQuestionUpdate` | +| PUT | `/meetings/{meetingId}/recordings/registrants/status` | Update a registrant's status | `meetingRecordingRegistrantStatus` | +| GET | `/meetings/{meetingId}/recordings/settings` | Get meeting recording settings | `recordingSettingUpdate` | +| PATCH | `/meetings/{meetingId}/recordings/settings` | Update meeting recording settings | `recordingSettingsUpdate` | +| DELETE | `/meetings/{meetingId}/recordings/{recordingId}` | Delete a recording file for a meeting or webinar | `recordingDeleteOne` | +| PUT | `/meetings/{meetingId}/recordings/{recordingId}/status` | Recover a single recording | `recordingStatusUpdateOne` | +| DELETE | `/meetings/{meetingId}/transcript` | Delete a meeting or webinar transcript | `DeleteMeetingTranscript` | +| GET | `/meetings/{meetingId}/transcript` | Get a meeting transcript | `GetMeetingTranscript` | +| PUT | `/meetings/{meetingUUID}/recordings/status` | Recover meeting recordings | `recordingStatusUpdate` | +| GET | `/users/{userId}/recordings` | List all recordings | `recordingsList` | + +### Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/devices` | List devices | `listDevices` | +| POST | `/devices` | Add a new device | `addDevice` | +| GET | `/devices/groups` | Get ZDM group info | `Getzdmgroupinfo` | +| POST | `/devices/zpa/assignment` | Assign a device to a user or commonarea | `Assigndevicetoauser/commonarea` | +| GET | `/devices/zpa/settings` | Get Zoom Phone Appliance settings by user ID | `GetZpaDeviceListProfileSettingOfaUser` | +| POST | `/devices/zpa/upgrade` | Upgrade ZPA firmware or app | `UpgradeZpas/app` | +| DELETE | `/devices/zpa/vendors/{vendor}/mac_addresses/{macAddress}` | Delete ZPA device by vendor and mac address | `DeleteZpaDeviceByVendorAndMacAddress` | +| GET | `/devices/zpa/zdm_groups/{zdmGroupId}/versions` | Get ZPA version info | `GetZpaVersioninfo` | +| DELETE | `/devices/{deviceId}` | Delete device | `deleteDevice` | +| GET | `/devices/{deviceId}` | Get device detail | `getDevice` | +| PATCH | `/devices/{deviceId}` | Change device | `updateDevice` | +| PATCH | `/devices/{deviceId}/assign_group` | Assign a device to a group | `assginGroup` | +| PATCH | `/devices/{deviceId}/assignment` | Change device association | `changeDeviceAssociation` | + +### H323 Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/h323/devices` | List H.323/SIP devices | `deviceList` | +| POST | `/h323/devices` | Create a H.323/SIP device | `deviceCreate` | +| DELETE | `/h323/devices/{deviceId}` | Delete a H.323/SIP device | `deviceDelete` | +| PATCH | `/h323/devices/{deviceId}` | Update a H.323/SIP device | `deviceUpdate` | + +### In-Meeting Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/open_apps` | Delete a meeting app | `meetingAppDelete` | +| POST | `/meetings/{meetingId}/open_apps` | Add a meeting app | `meetingAppAdd` | + +### In-Meeting Features + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/live_meetings/{meetingId}/chat/messages/{messageId}` | Delete a live meeting message | `deleteMeetingChatMessageById` | +| PATCH | `/live_meetings/{meetingId}/chat/messages/{messageId}` | Update a live meeting message | `updateMeetingChatMessageById` | +| PATCH | `/live_meetings/{meetingId}/events` | In-meeting controls | `inMeetingControl` | +| GET | `/meetings/{meetingId}/jointoken/local_recording` | Get a meeting's join token for local recording | `meetingLocalRecordingJoinToken` | +| GET | `/meetings/{meetingId}/token` | Get meeting's token | `meetingToken` | + +### Invitation & Registration + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/meetings/{meetingId}/batch_registrants` | Perform batch registration | `addBatchRegistrants` | +| GET | `/meetings/{meetingId}/invitation` | Get meeting invitation | `meetingInvitation` | +| POST | `/meetings/{meetingId}/invite_links` | Create a meeting's invite links | `meetingInviteLinksCreate` | +| GET | `/meetings/{meetingId}/registrants` | List meeting registrants | `meetingRegistrants` | +| POST | `/meetings/{meetingId}/registrants` | Add a meeting registrant | `meetingRegistrantCreate` | +| GET | `/meetings/{meetingId}/registrants/questions` | List registration questions | `meetingRegistrantsQuestionsGet` | +| PATCH | `/meetings/{meetingId}/registrants/questions` | Update registration questions | `meetingRegistrantQuestionUpdate` | +| PUT | `/meetings/{meetingId}/registrants/status` | Update registrant's status | `meetingRegistrantStatus` | +| DELETE | `/meetings/{meetingId}/registrants/{registrantId}` | Delete a meeting registrant | `meetingregistrantdelete` | +| GET | `/meetings/{meetingId}/registrants/{registrantId}` | Get a meeting registrant | `meetingRegistrantGet` | + +### Live streaming + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/meetings/{meetingId}/jointoken/live_streaming` | Get a meeting's join token for live streaming | `meetingLiveStreamingJoinToken` | +| GET | `/meetings/{meetingId}/livestream` | Get livestream details | `getMeetingLiveStreamDetails` | +| PATCH | `/meetings/{meetingId}/livestream` | Update a livestream | `meetingLiveStreamUpdate` | +| PATCH | `/meetings/{meetingId}/livestream/status` | Update livestream status | `meetingLiveStreamStatusUpdate` | + +### Meetings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/live_meetings/{meetingId}/rtms_app/status` | Update participant Real-Time Media Streams (RTMS) app status | `meetingRTMSStatusUpdate` | +| DELETE | `/meetings/{meetingId}` | Delete a meeting | `meetingDelete` | +| GET | `/meetings/{meetingId}` | Get a meeting | `meeting` | +| PATCH | `/meetings/{meetingId}` | Update a meeting | `meetingUpdate` | +| POST | `/meetings/{meetingId}/sip_dialing` | Get a meeting SIP URI with passcode | `getSipDialingWithPasscode` | +| PUT | `/meetings/{meetingId}/status` | Update meeting status | `meetingStatus` | +| GET | `/past_meetings/{meetingId}` | Get past meeting details | `pastMeetingDetails` | +| GET | `/past_meetings/{meetingId}/instances` | List past meeting instances | `pastMeetings` | +| GET | `/past_meetings/{meetingId}/participants` | Get past meeting participants | `pastMeetingParticipants` | +| GET | `/past_meetings/{meetingId}/qa` | List past meetings' Q&A | `listPastMeetingQA` | +| GET | `/users/{userId}/meetings` | List meetings | `meetings` | +| POST | `/users/{userId}/meetings` | Create a meeting | `meetingCreate` | +| GET | `/users/{userId}/upcoming_meetings` | List upcoming meetings | `listUpcomingMeeting` | + +### PAC + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users/{userId}/pac` | List a user's PAC accounts | `userPACs` | + +### Polls + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/meetings/{meetingId}/batch_polls` | Perform batch poll creation | `createBatchPolls` | +| GET | `/meetings/{meetingId}/polls` | List meeting polls | `meetingPolls` | +| POST | `/meetings/{meetingId}/polls` | Create a meeting poll | `meetingPollCreate` | +| DELETE | `/meetings/{meetingId}/polls/{pollId}` | Delete a meeting poll | `meetingPollDelete` | +| GET | `/meetings/{meetingId}/polls/{pollId}` | Get a meeting poll | `meetingPollGet` | +| PUT | `/meetings/{meetingId}/polls/{pollId}` | Update a meeting poll | `meetingPollUpdate` | +| GET | `/past_meetings/{meetingId}/polls` | List past meeting's poll results | `listPastMeetingPolls` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/report/activities` | Get sign In / sign out activity report | `reportSignInSignOutActivities` | +| GET | `/report/billing` | Get billing reports | `getBillingReport` | +| GET | `/report/billing/invoices` | Get billing invoice reports | `getBillingInvoicesReports` | +| GET | `/report/cloud_recording` | Get cloud recording usage report | `reportCloudRecording` | +| GET | `/report/daily` | Get daily usage report | `reportDaily` | +| GET | `/report/history_meetings` | Get history meeting and webinar list | `Gethistorymeetingandwebinarlist` | +| GET | `/report/meeting_activities` | Get a meeting activities report | `reportMeetingactivitylogs` | +| GET | `/report/meetings/{meetingId}` | Get meeting detail reports | `reportMeetingDetails` | +| GET | `/report/meetings/{meetingId}/participants` | Get meeting participant reports | `reportMeetingParticipants` | +| GET | `/report/meetings/{meetingId}/polls` | Get meeting poll reports | `reportMeetingPolls` | +| GET | `/report/meetings/{meetingId}/qa` | Get meeting Q&A report | `reportMeetingQA` | +| GET | `/report/meetings/{meetingId}/survey` | Get meeting survey report | `reportMeetingSurvey` | +| GET | `/report/operationlogs` | Get operation logs report | `reportOperationLogs` | +| GET | `/report/remote_support` | Get remote support report | `Getremotesupportreport` | +| GET | `/report/telephone` | Get telephone reports | `reportTelephone` | +| GET | `/report/upcoming_events` | Get upcoming events report | `reportUpcomingEvents` | +| GET | `/report/users` | Get active or inactive host reports | `reportUsers` | +| GET | `/report/users/{userId}/meetings` | Get meeting reports | `reportMeetings` | +| GET | `/report/webinars/{webinarId}` | Get webinar detail reports | `reportWebinarDetails` | +| GET | `/report/webinars/{webinarId}/participants` | Get webinar participant reports | `reportWebinarParticipants` | +| GET | `/report/webinars/{webinarId}/polls` | Get webinar poll reports | `reportWebinarPolls` | +| GET | `/report/webinars/{webinarId}/qa` | Get webinar Q&A report | `reportWebinarQA` | +| GET | `/report/webinars/{webinarId}/survey` | Get webinar survey report | `reportWebinarSurvey` | + +### SIP Phone + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/sip_phones/phones` | List SIP phones | `ListSIPPhonePhones` | +| POST | `/sip_phones/phones` | Enable SIP phone | `EnableSIPPhonePhones` | +| DELETE | `/sip_phones/phones/{phoneId}` | Delete SIP phone | `deleteSIPPhonePhones` | +| PATCH | `/sip_phones/phones/{phoneId}` | Update SIP phone | `UpdateSIPPhonePhones` | + +### Summaries + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/meetings/meeting_summaries` | List an account's meeting or webinar summaries | `Listmeetingsummaries` | +| DELETE | `/meetings/{meetingId}/meeting_summary` | Delete a meeting or webinar summary | `Deletemeetingorwebinarsummary` | +| GET | `/meetings/{meetingId}/meeting_summary` | Get a meeting or webinar summary | `Getameetingsummary` | + +### Surveys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/survey` | Delete a meeting survey | `meetingSurveyDelete` | +| GET | `/meetings/{meetingId}/survey` | Get a meeting survey | `meetingSurveyGet` | +| PATCH | `/meetings/{meetingId}/survey` | Update a meeting survey | `meetingSurveyUpdate` | + +### Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users/{userId}/meeting_templates` | List meeting templates | `listMeetingTemplates` | +| POST | `/users/{userId}/meeting_templates` | Create a meeting template from an existing meeting | `meetingTemplateCreate` | + +### Tracking Field + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tracking_fields` | List tracking fields | `trackingfieldList` | +| POST | `/tracking_fields` | Create a tracking field | `trackingfieldCreate` | +| DELETE | `/tracking_fields/{fieldId}` | Delete a tracking field | `trackingfieldDelete` | +| GET | `/tracking_fields/{fieldId}` | Get a tracking field | `trackingfieldGet` | +| PATCH | `/tracking_fields/{fieldId}` | Update a tracking field | `trackingfieldUpdate` | + +### TSP + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tsp` | Get account's TSP information | `tsp` | +| PATCH | `/tsp` | Update an account's TSP information | `tspUpdate` | +| GET | `/users/{userId}/tsp` | List user's TSP accounts | `userTSPs` | +| POST | `/users/{userId}/tsp` | Add a user's TSP account | `userTSPCreate` | +| PATCH | `/users/{userId}/tsp/settings` | Set global dial-in URL for a TSP user | `tspUrlUpdate` | +| DELETE | `/users/{userId}/tsp/{tspId}` | Delete a user's TSP account | `userTSPDelete` | +| GET | `/users/{userId}/tsp/{tspId}` | Get a user's TSP account | `userTSP` | +| PATCH | `/users/{userId}/tsp/{tspId}` | Update a TSP account | `userTSPUpdate` | + +### Webinars + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/live_webinars/{webinarId}/chat/messages/{messageId}` | Delete a live webinar message | `deleteWebinarChatMessageById` | +| GET | `/past_webinars/{webinarId}/absentees` | Get webinar absentees | `webinarAbsentees` | +| GET | `/past_webinars/{webinarId}/instances` | List past webinar instances | `pastWebinars` | +| GET | `/past_webinars/{webinarId}/participants` | List webinar participants | `listWebinarParticipants` | +| GET | `/past_webinars/{webinarId}/polls` | List past webinar poll results | `listPastWebinarPollResults` | +| GET | `/past_webinars/{webinarId}/qa` | List Q&As of a past webinar | `listPastWebinarQA` | +| GET | `/users/{userId}/webinar_templates` | List webinar templates | `listWebinarTemplates` | +| POST | `/users/{userId}/webinar_templates` | Create a webinar template | `webinarTemplateCreate` | +| GET | `/users/{userId}/webinars` | List webinars | `webinars` | +| POST | `/users/{userId}/webinars` | Create a webinar | `webinarCreate` | +| DELETE | `/webinars/{webinarId}` | Delete a webinar | `webinarDelete` | +| GET | `/webinars/{webinarId}` | Get a webinar | `webinar` | +| PATCH | `/webinars/{webinarId}` | Update a webinar | `webinarUpdate` | +| POST | `/webinars/{webinarId}/batch_registrants` | Perform batch registration | `addBatchWebinarRegistrants` | +| GET | `/webinars/{webinarId}/branding` | Get webinar's session branding | `getWebinarBranding` | +| DELETE | `/webinars/{webinarId}/branding/name_tags` | Delete a webinar's branding name tag | `deleteWebinarBrandingNameTag` | +| POST | `/webinars/{webinarId}/branding/name_tags` | Create a webinar's branding name tag | `createWebinarBrandingNameTag` | +| PATCH | `/webinars/{webinarId}/branding/name_tags/{nameTagId}` | Update a webinar's branding name tag | `updateWebinarBrandingNameTag` | +| DELETE | `/webinars/{webinarId}/branding/virtual_backgrounds` | Delete a webinar's branding virtual backgrounds | `deleteWebinarBrandingVB` | +| PATCH | `/webinars/{webinarId}/branding/virtual_backgrounds` | Set webinar's default branding virtual background | `setWebinarBrandingVB` | +| POST | `/webinars/{webinarId}/branding/virtual_backgrounds` | Upload a webinar's branding virtual background | `uploadWebinarBrandingVB` | +| DELETE | `/webinars/{webinarId}/branding/wallpaper` | Delete a webinar's branding wallpaper | `deleteWebinarBrandingWallpaper` | +| POST | `/webinars/{webinarId}/branding/wallpaper` | Upload a webinar's branding wallpaper | `uploadWebinarBrandingWallpaper` | +| POST | `/webinars/{webinarId}/invite_links` | Create webinar's invite links | `webinarInviteLinksCreate` | +| GET | `/webinars/{webinarId}/jointoken/live_streaming` | Get a webinar's join token for live streaming | `webinarLiveStreamingJoinToken` | +| GET | `/webinars/{webinarId}/jointoken/local_archiving` | Get a webinar's archive token for local archiving | `webinarLocalArchivingArchiveToken` | +| GET | `/webinars/{webinarId}/jointoken/local_recording` | Get a webinar's join token for local recording | `webinarLocalRecordingJoinToken` | +| GET | `/webinars/{webinarId}/livestream` | Get live stream details | `getWebinarLiveStreamDetails` | +| PATCH | `/webinars/{webinarId}/livestream` | Update a live stream | `webinarLiveStreamUpdate` | +| PATCH | `/webinars/{webinarId}/livestream/status` | Update live stream status | `webinarLiveStreamStatusUpdate` | +| DELETE | `/webinars/{webinarId}/panelists` | Remove all panelists | `webinarPanelistsDelete` | +| GET | `/webinars/{webinarId}/panelists` | List panelists | `webinarPanelists` | +| POST | `/webinars/{webinarId}/panelists` | Add panelists | `webinarPanelistCreate` | +| DELETE | `/webinars/{webinarId}/panelists/{panelistId}` | Remove a panelist | `webinarPanelistDelete` | +| GET | `/webinars/{webinarId}/polls` | List a webinar's polls | `webinarPolls` | +| POST | `/webinars/{webinarId}/polls` | Create a webinar's poll | `webinarPollCreate` | +| DELETE | `/webinars/{webinarId}/polls/{pollId}` | Delete a webinar poll | `webinarPollDelete` | +| GET | `/webinars/{webinarId}/polls/{pollId}` | Get a webinar poll | `webinarPollGet` | +| PUT | `/webinars/{webinarId}/polls/{pollId}` | Update a webinar poll | `webinarPollUpdate` | +| GET | `/webinars/{webinarId}/registrants` | List webinar registrants | `webinarRegistrants` | +| POST | `/webinars/{webinarId}/registrants` | Add a webinar registrant | `webinarRegistrantCreate` | +| GET | `/webinars/{webinarId}/registrants/questions` | List registration questions | `webinarRegistrantsQuestionsGet` | +| PATCH | `/webinars/{webinarId}/registrants/questions` | Update registration questions | `webinarRegistrantQuestionUpdate` | +| PUT | `/webinars/{webinarId}/registrants/status` | Update registrant's status | `webinarRegistrantStatus` | +| DELETE | `/webinars/{webinarId}/registrants/{registrantId}` | Delete a webinar registrant | `deleteWebinarRegistrant` | +| GET | `/webinars/{webinarId}/registrants/{registrantId}` | Get a webinar registrant | `webinarRegistrantGet` | +| POST | `/webinars/{webinarId}/sip_dialing` | Get a webinar SIP URI with passcode | `getWebinarSipDialingWithPasscode` | +| PUT | `/webinars/{webinarId}/status` | Update webinar status | `webinarStatus` | +| DELETE | `/webinars/{webinarId}/survey` | Delete a webinar survey | `webinarSurveyDelete` | +| GET | `/webinars/{webinarId}/survey` | Get a webinar survey | `webinarSurveyGet` | +| PATCH | `/webinars/{webinarId}/survey` | Update a webinar survey | `webinarSurveyUpdate` | +| GET | `/webinars/{webinarId}/token` | Get webinar's token | `webinarToken` | +| GET | `/webinars/{webinarId}/tracking_sources` | Get webinar tracking sources | `getTrackingSources` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/number-management.md b/partner-built/zoom-plugin/skills/rest-api/references/number-management.md new file mode 100644 index 00000000..b1461158 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/number-management.md @@ -0,0 +1,94 @@ +# Zoom Number Management API + +Authoritative endpoint inventory for Number Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/number-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 17 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Cloud Peering Provider Exchange | 5 | +| Phone Numbers | 6 | +| Phone Plan | 1 | +| Setting | 4 | +| SMS Campaigns | 4 | +| SMS Consent | 8 | + +## Endpoints by Tag + +### Cloud Peering Provider Exchange + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/carrier/peering_numbers` | List peering phone numbers for provider | `Listpeeringphonenumbersforprovider` | +| DELETE | `/number_management/peering_numbers` | Remove peering phone numbers | `deletePeeringPhoneNumbers` | +| GET | `/number_management/peering_numbers` | List peering phone numbers | `listPeeringPhoneNumbers` | +| PATCH | `/number_management/peering_numbers` | Update peering phone numbers | `updatePeeringPhoneNumbers` | +| POST | `/number_management/peering_numbers` | Add peering phone numbers | `addPeeringPhoneNumbers` | + +### Phone Numbers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/number_management/allocation` | Allocate/Unallocate phone numbers | `AllocateNumber` | +| POST | `/number_management/byoc_numbers` | Add BYOC phone numbers | `AddBYOCPhoneNumber` | +| DELETE | `/number_management/numbers` | Delete phone numbers | `DeleteNumbers` | +| GET | `/number_management/numbers` | List phone numbers | `listPhoneNumbers` | +| GET | `/number_management/numbers/{phoneNumberId}` | Get a phone number | `getPhoneNumber` | +| PATCH | `/number_management/numbers/{phoneNumberId}` | Update a phone number | `updatePhoneNumberDetail` | + +### Phone Plan + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/plan` | List phone number plan information | `Listphonenumberplaninformation` | + +### Setting + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/ported_numbers/orders` | List ported numbers | `Listportednumbers` | +| GET | `/number_management/ported_numbers/orders/{orderId}` | Get ported numbers details | `Getportednumbersdetails` | +| GET | `/number_management/sip_groups` | List SIP groups | `ListSIPgroups` | +| GET | `/number_management/sip_trunks` | List BYOC SIP trunks | `ListBYOCSIPtrunks` | + +### SMS Campaigns + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/sms_campaigns` | List SMS campaigns | `listAccountSMSCampaigns` | +| GET | `/number_management/sms_campaigns/{smsCampaignId}` | Get an SMS campaign | `GetSMSCampaign` | +| DELETE | `/number_management/sms_campaigns/{smsCampaignId}/phone_numbers` | Unassign phone number from SMS campaign | `unassignCampaignPhoneNumber` | +| POST | `/number_management/sms_campaigns/{smsCampaignId}/phone_numbers` | Assign a phone number to SMS campaign | `assignCampaignPhoneNumbers` | + +### SMS Consent + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/number_management/sms_consent` | Delete SMS consent policies | `DeleteSMSConsents` | +| GET | `/number_management/sms_consent` | List SMS consent policies | `ListSMSConsents` | +| POST | `/number_management/sms_consent` | Create an SMS consent policy | `CreateSMSConsent` | +| GET | `/number_management/sms_consent/{consentId}` | Get an SMS consent policy | `GetSMSConsent` | +| PATCH | `/number_management/sms_consent/{consentId}` | Update an SMS consent policy | `UpdateSMSConsent` | +| DELETE | `/number_management/sms_consent/{consentId}/phone_numbers` | Unassign phone numbers from SMS consent policy | `UnassignPhoneNumbersFromConsent` | +| GET | `/number_management/sms_consent/{consentId}/phone_numbers` | List phone numbers assigned to SMS consent policy | `ListConsentPhoneNumbers` | +| POST | `/number_management/sms_consent/{consentId}/phone_numbers` | Assign phone numbers to SMS consent policy | `AssignPhoneNumbersToConsent` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/openapi.md b/partner-built/zoom-plugin/skills/rest-api/references/openapi.md new file mode 100644 index 00000000..1005525b --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/openapi.md @@ -0,0 +1,166 @@ +# OpenAPI / Swagger Specifications + +Zoom provides OpenAPI specifications for API client generation and tooling integration. + +## Official Specifications + +### Zoom API v2 (Current) + +| Property | Value | +|----------|-------| +| Format | Swagger 2.0 (JSON) | +| Status | **Deprecated** - Last updated November 2018 | +| Coverage | ~103 endpoints (subset of full API) | +| Download | [openapi.v2.json](https://raw.githubusercontent.com/zoom/api/442998230a148f403c3d1de1fe7aa54937354fa9/openapi.v2.json) | +| Repository | [github.com/zoom/api](https://github.com/zoom/api) | + +### Zoom API v1 (Legacy) + +| Property | Value | +|----------|-------| +| Format | Swagger 2.0 (JSON) | +| Status | **Deprecated** | +| Coverage | ~93 endpoints | +| Download | [openapi.v2.json](https://raw.githubusercontent.com/zoom/api-v1/5478bfe304a827f97acfed9aa5a0ba840b8d1aa9/openapi.v2.json) | +| Repository | [github.com/zoom/api-v1](https://github.com/zoom/api-v1) | + +## Coverage Limitations + +The official OpenAPI specs only cover a **small subset** of Zoom's 600+ endpoints: + +| Covered | NOT Covered | +|---------|-------------| +| Meetings | Phone API | +| Users | Team Chat API | +| Accounts | Mail API | +| Groups | Calendar API | +| Reports | Rooms API | +| Webinars | Clips API | +| H.323 Devices | Whiteboard API | +| IM Groups | Contact Center API | +| | AI Companion API | +| | 20+ other modern APIs | + +## Recommended Alternative: Zoom Rivet + +Instead of using the outdated OpenAPI specs, Zoom recommends using **Zoom Rivet** - their official API client library. + +### Installation + +```bash +npm install @zoom/rivet +``` + +### Usage + +```typescript +import Zoom from '@zoom/rivet'; + +const zoom = new Zoom({ + accountId: process.env.ZOOM_ACCOUNT_ID, + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, +}); + +// Create a meeting +const meeting = await zoom.meetings.create({ + userId: 'me', + body: { + topic: 'My Meeting', + type: 2, + duration: 60, + }, +}); + +// List users +const users = await zoom.users.list(); + +// Get recordings +const recordings = await zoom.cloudRecordings.list({ + userId: 'me', + from: '2024-01-01', + to: '2024-01-31', +}); +``` + +### Why Rivet Over OpenAPI? + +| Aspect | OpenAPI Specs | Zoom Rivet | +|--------|---------------|------------| +| Coverage | ~103 endpoints | Full API | +| Maintenance | Deprecated (2018) | Actively maintained | +| Type Safety | Requires codegen | Built-in TypeScript | +| Auth Handling | Manual | Automatic token management | +| Pagination | Manual | Built-in helpers | +| Rate Limiting | Manual | Built-in retry logic | + +## Using OpenAPI Specs Anyway + +If you still need the OpenAPI specs (e.g., for custom tooling), here's how to use them: + +### Download the Spec + +```bash +# Download Zoom API v2 spec +curl -o zoom-api-v2.json \ + https://raw.githubusercontent.com/zoom/api/442998230a148f403c3d1de1fe7aa54937354fa9/openapi.v2.json +``` + +### Generate TypeScript Client + +```bash +# Using openapi-generator +npm install @openapitools/openapi-generator-cli -g + +openapi-generator-cli generate \ + -i zoom-api-v2.json \ + -g typescript-fetch \ + -o ./zoom-client +``` + +### Generate Python Client + +```bash +openapi-generator-cli generate \ + -i zoom-api-v2.json \ + -g python \ + -o ./zoom-client-python +``` + +### Known Issues with Code Generation + +The Zoom OpenAPI specs have known issues that may cause errors during code generation: + +| Issue | Workaround | +|-------|------------| +| Enum type mismatches | Manually fix integer/string enum definitions | +| Missing required fields | Add required fields to generated models | +| Invalid syntax | Validate and fix JSON before generation | +| Outdated endpoints | Supplement with manual API calls for new endpoints | + +## Postman Collection + +For interactive API exploration, Zoom provides a Postman collection: + +- **Postman Collection**: https://marketplace.zoom.us/docs/api-reference/postman + +```bash +# Import to Postman +# 1. Open Postman +# 2. Click Import +# 3. Enter URL: https://www.postman.com/zoom-developer/zoom-developer-api +``` + +## API Reference Documentation + +For the most up-to-date API documentation, use the official reference: + +- **REST API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/ +- **API Changelog**: https://developers.zoom.us/changelog/ + +## Resources + +- [Zoom Rivet (Official SDK)](https://github.com/zoom/rivet-javascript) +- [OpenAPI Generator](https://openapi-generator.tech/) +- [Swagger Editor](https://editor.swagger.io/) +- [Postman Collection](https://marketplace.zoom.us/docs/api-reference/postman) diff --git a/partner-built/zoom-plugin/skills/rest-api/references/phone.md b/partner-built/zoom-plugin/skills/rest-api/references/phone.md new file mode 100644 index 00000000..80868dc1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/phone.md @@ -0,0 +1,666 @@ +# Zoom Phone API + +Authoritative endpoint inventory for Phone. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/phone/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 354 | +| Path templates | 213 | +| Tags | 47 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Accounts | 4 | +| Alerts | 5 | +| Audio Library | 6 | +| Auto Receptionists | 13 | +| Billing Account | 2 | +| Blocked List | 5 | +| Call Handling | 4 | +| Call Logs | 15 | +| Call Queues | 17 | +| Carrier Reseller | 4 | +| Common Areas | 17 | +| Dashboard | 11 | +| Device Line Keys | 2 | +| Dial by Name Directory | 6 | +| Emergency Addresses | 5 | +| Emergency Service Locations | 6 | +| External Contacts | 5 | +| Fax | 8 | +| Firmware Update Rules | 6 | +| Group Call Pickup | 8 | +| Groups | 3 | +| Inbound Blocked List | 10 | +| IVR | 2 | +| Line Keys | 3 | +| Monitoring Groups | 9 | +| Outbound Calling | 24 | +| Phone Devices | 11 | +| Phone Numbers | 8 | +| Phone Plans | 2 | +| Phone Roles | 11 | +| Private Directory | 4 | +| Provider Exchange | 5 | +| Provision Templates | 5 | +| Recordings | 8 | +| Reports | 4 | +| Routing Rules | 5 | +| Setting Templates | 4 | +| Settings | 8 | +| Shared Line Appearance | 1 | +| Shared Line Group | 16 | +| Sites | 12 | +| SMS | 7 | +| SMS Campaign | 7 | +| SMS Consent | 1 | +| Users | 18 | +| Voicemails | 7 | +| Zoom Rooms | 10 | + +## Endpoints by Tag + +### Accounts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/account_settings` | List an account's Zoom Phone settings | `listZoomPhoneAccountSettings` | +| DELETE | `/phone/outbound_caller_id/customized_numbers` | Delete phone numbers for an account's customized outbound caller ID | `deleteOutboundCallerNumbers` | +| GET | `/phone/outbound_caller_id/customized_numbers` | List an account's customized outbound caller ID phone numbers | `listCustomizeOutboundCallerNumbers` | +| POST | `/phone/outbound_caller_id/customized_numbers` | Add phone numbers for an account's customized outbound caller ID | `addOutboundCallerNumbers` | + +### Alerts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/alert_settings` | List alert settings with paging query | `ListAlertSettingsWithPagingQuery` | +| POST | `/phone/alert_settings` | Add an alert setting | `AddAnAlertSetting` | +| DELETE | `/phone/alert_settings/{alertSettingId}` | Delete an alert setting | `DeleteAnAlertSetting` | +| GET | `/phone/alert_settings/{alertSettingId}` | Get alert setting details | `GetAlertSettingDetails` | +| PATCH | `/phone/alert_settings/{alertSettingId}` | Update an alert setting | `UpdateAnAlertSetting` | + +### Audio Library + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/audios/{audioId}` | Delete an audio item | `DeleteAudioItem` | +| GET | `/phone/audios/{audioId}` | Get an audio item | `GetAudioItem` | +| PATCH | `/phone/audios/{audioId}` | Update an audio item | `UpdateAudioItem` | +| GET | `/phone/users/{userId}/audios` | List audio items | `ListAudioItems` | +| POST | `/phone/users/{userId}/audios` | Add an audio item for text-to-speech conversion | `AddAnAudio` | +| POST | `/phone/users/{userId}/audios/batch` | Add audio items | `AddAudioItem` | + +### Auto Receptionists + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/auto_receptionists` | List auto receptionists | `listAutoReceptionists` | +| POST | `/phone/auto_receptionists` | Add an auto receptionist | `addAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}` | Delete a non-primary auto receptionist | `deleteAutoReceptionist` | +| GET | `/phone/auto_receptionists/{autoReceptionistId}` | Get an auto receptionist | `getAutoReceptionistDetail` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}` | Update an auto receptionist | `updateAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers` | Unassign all phone numbers | `unassignAllPhoneNumsAutoReceptionist` | +| POST | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers` | Assign phone numbers | `assignPhoneNumbersAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unassignAPhoneNumAutoReceptionist` | +| GET | `/phone/auto_receptionists/{autoReceptionistId}/policies` | Get an auto receptionist policy | `getAutoReceptionistsPolicy` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/policies` | Update an auto receptionist policy | `updateAutoReceptionistPolicy` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Delete a policy subsetting | `DeletePolicy` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Update a policy subsetting | `updatePolicy` | +| POST | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Add a policy subsetting | `AddPolicy` | + +### Billing Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/billing_accounts` | List billing accounts | `listBillingAccount` | +| GET | `/phone/billing_accounts/{billingAccountId}` | Get billing account details | `GetABillingAccount` | + +### Blocked List + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/blocked_list` | List blocked lists | `listBlockedList` | +| POST | `/phone/blocked_list` | Create a blocked list | `addAnumberToBlockedList` | +| DELETE | `/phone/blocked_list/{blockedListId}` | Delete a blocked list | `deleteABlockedList` | +| GET | `/phone/blocked_list/{blockedListId}` | Get blocked list details | `getABlockedList` | +| PATCH | `/phone/blocked_list/{blockedListId}` | Update a blocked list | `updateBlockedList` | + +### Call Handling + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/extension/{extensionId}/call_handling/settings` | Get call handling settings | `getCallHandling` | +| DELETE | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Delete a call handling setting | `deleteCallHandling` | +| PATCH | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Update a call handling setting | `updateCallHandling` | +| POST | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Add a call handling setting | `addCallHandling` | + +### Call Logs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_element/{callElementId}` | Get call element | `getCallElement` | +| GET | `/phone/call_history` | Get account's call history | `accountCallHistory` | +| GET | `/phone/call_history/{callHistoryUuid}` | Get call history | `getCallPath` | +| PATCH | `/phone/call_history/{callLogId}/client_code` | Add a client code to a call history | `addClientCodeToCallHistory` | +| GET | `/phone/call_history_detail/{callHistoryId}` | Get call history detail | `getCallHistoryDetail` | +| GET | `/phone/call_logs` | Get account's call logs | `accountCallLogs` | +| GET | `/phone/call_logs/{callLogId}` | Get call log details | `getCallLogDetails` | +| PUT | `/phone/call_logs/{callLogId}/client_code` | Add a client code to a call log | `addClientCodeToCallLog` | +| GET | `/phone/user/{userId}/ai_call_summary/{aiCallSummaryId}` | Get User AI Call Summary Detail | `getUserAICallSummary` | +| GET | `/phone/users/{userId}/call_history` | Get user's call history | `phoneUserCallHistory` | +| GET | `/phone/users/{userId}/call_history/sync` | Sync user's call history | `syncUserCallHistory` | +| DELETE | `/phone/users/{userId}/call_history/{callLogId}` | Delete a user's call history | `deleteUserCallHistory` | +| GET | `/phone/users/{userId}/call_logs` | Get user's call logs | `phoneUserCallLogs` | +| GET | `/phone/users/{userId}/call_logs/sync` | Sync user's call logs | `syncUserCallLogs` | +| DELETE | `/phone/users/{userId}/call_logs/{callLogId}` | Delete a user's call log | `deleteCallLog` | + +### Call Queues + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_queue_analytics` | List call queue analytics | `callqueueanalytics` | +| GET | `/phone/call_queues` | List call queues | `listCallQueues` | +| POST | `/phone/call_queues` | Create a call queue | `createCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}` | Delete a call queue | `deleteACallQueue` | +| GET | `/phone/call_queues/{callQueueId}` | Get call queue details | `getACallQueue` | +| PATCH | `/phone/call_queues/{callQueueId}` | Update call queue details | `updateCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/members` | Unassign all members | `unassignAllMembers` | +| GET | `/phone/call_queues/{callQueueId}/members` | List call queue members | `listCallQueueMembers` | +| POST | `/phone/call_queues/{callQueueId}/members` | Add members to a call queue | `addMembersToCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/members/{memberId}` | Unassign a member | `unassignMemberFromCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/phone_numbers` | Unassign all phone numbers | `unassignAPhoneNumCallQueue` | +| POST | `/phone/call_queues/{callQueueId}/phone_numbers` | Assign numbers to a call queue | `assignPhoneToCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unAssignPhoneNumCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Delete a CQ policy setting | `removeCQPolicySubSetting` | +| PATCH | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Update a call queue's policy subsetting | `updateCQPolicySubSetting` | +| POST | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Add a policy subsetting to a call queue | `addCQPolicySubSetting` | +| GET | `/phone/call_queues/{callQueueId}/recordings` | Get call queue recordings | `getCallQueueRecordings` | + +### Carrier Reseller + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/carrier_reseller/numbers` | List phone numbers | `listCRPhoneNumbers` | +| PATCH | `/phone/carrier_reseller/numbers` | Activate phone numbers | `activeCRPhoneNumbers` | +| POST | `/phone/carrier_reseller/numbers` | Create phone numbers | `createCRPhoneNumbers` | +| DELETE | `/phone/carrier_reseller/numbers/{number}` | Delete a phone number | `deleteCRPhoneNumber` | + +### Common Areas + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/common_areas` | List common areas | `listCommonAreas` | +| POST | `/phone/common_areas` | Add a common area | `addCommonArea` | +| POST | `/phone/common_areas/activation_code` | Generate activation codes for common areas | `Generateactivationcodesforcommonareas` | +| GET | `/phone/common_areas/activation_codes` | List activation codes | `listActivationCodes` | +| POST | `/phone/common_areas/template_id/{templateId}` | Apply template to common areas | `ApplyTemplatetoCommonAreas` | +| DELETE | `/phone/common_areas/{commonAreaId}` | Delete a common area | `deleteCommonArea` | +| GET | `/phone/common_areas/{commonAreaId}` | Get common area details | `getACommonArea` | +| PATCH | `/phone/common_areas/{commonAreaId}` | Update common area | `updateCommonArea` | +| POST | `/phone/common_areas/{commonAreaId}/calling_plans` | Assign calling plans to a common area | `assignCallingPlansToCommonArea` | +| DELETE | `/phone/common_areas/{commonAreaId}/calling_plans/{type}` | Unassign a calling plan from the common area | `unassignCallingPlansFromCommonArea` | +| POST | `/phone/common_areas/{commonAreaId}/phone_numbers` | Assign phone numbers to a common area | `assignPhoneNumbersToCommonArea` | +| DELETE | `/phone/common_areas/{commonAreaId}/phone_numbers/{phoneNumberId}` | Unassign phone numbers from common area | `unassignPhoneNumbersFromCommonArea` | +| PATCH | `/phone/common_areas/{commonAreaId}/pin_code` | Update common area pin code | `UpdateCommonAreaPinCode` | +| GET | `/phone/common_areas/{commonAreaId}/settings` | Get common area settings | `getCommonAreaSettings` | +| DELETE | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Delete common area setting | `deleteCommonAreaSetting` | +| PATCH | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Update common area setting | `UpdateCommonAreaSetting` | +| POST | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Add common area setting | `AddCommonAreaSetting` | + +### Dashboard + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/metrics/call_logs` | List call logs | `listCallLogsMetrics` | +| GET | `/phone/metrics/call_logs/{callId}/qos` | Get call QoS | `getCallQoS` | +| GET | `/phone/metrics/call_logs/{call_id}` | Get call details from call log | `getCallLogMetricsDetails` | +| GET | `/phone/metrics/emergency_services/default_emergency_address/users` | List default emergency address users | `listUserDefaultEmergencyAddress` | +| GET | `/phone/metrics/emergency_services/detectable_personal_location/users` | List detectable personal location users | `listUserDetectablePersonalLocation` | +| GET | `/phone/metrics/emergency_services/location_sharing_permission/users` | List users permission for location sharing | `listUserLocationSharingPermission` | +| GET | `/phone/metrics/emergency_services/nomadic_emergency_services/users` | List nomadic emergency services users | `listUserNomadicEmergencyServices` | +| GET | `/phone/metrics/emergency_services/realtime_location/devices` | List real time location for IP phones | `listPhoneRealtimelocation` | +| GET | `/phone/metrics/emergency_services/realtime_location/users` | List real time location for users | `listUserRealtimeLocation` | +| GET | `/phone/metrics/location_tracking` | List tracked locations | `listTrackedLocations` | +| GET | `/phone/metrics/past_calls` | List past call metrics | `listPastCallMetrics` | + +### Device Line Keys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/devices/{deviceId}/line_keys` | Get device line keys information | `listDeviceLineKeySetting` | +| PATCH | `/phone/devices/{deviceId}/line_keys` | Batch update device line key position | `batchUpdateDeviceLineKeySetting` | + +### Dial by Name Directory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/dial_by_name_directory/extensions` | Delete users from a directory | `DeleteUsersFromDirectory` | +| GET | `/phone/dial_by_name_directory/extensions` | List users in directory | `ListUsersFromDirectory` | +| POST | `/phone/dial_by_name_directory/extensions` | Add users to a directory | `AddUsersToDirectory` | +| DELETE | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | Delete users from a directory of a site | `DeleteUsersFromDirectoryBySite` | +| GET | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | List users in a directory by site | `ListUsersFromDirectoryBySite` | +| POST | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | Add users to a directory of a site | `AddUsersToDirectoryBySite` | + +### Emergency Addresses + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/emergency_addresses` | List emergency addresses | `listEmergencyAddresses` | +| POST | `/phone/emergency_addresses` | Add an emergency address | `addEmergencyAddress` | +| DELETE | `/phone/emergency_addresses/{emergencyAddressId}` | Delete an emergency address | `deleteEmergencyAddress` | +| GET | `/phone/emergency_addresses/{emergencyAddressId}` | Get emergency address details | `getEmergencyAddress` | +| PATCH | `/phone/emergency_addresses/{emergencyAddressId}` | Update an emergency address | `updateEmergencyAddress` | + +### Emergency Service Locations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/batch_locations` | Batch add emergency service locations | `batchAddLocations` | +| GET | `/phone/locations` | List emergency service locations | `listLocations` | +| POST | `/phone/locations` | Add an emergency service location | `addLocation` | +| DELETE | `/phone/locations/{locationId}` | Delete an emergency location | `deleteLocation` | +| GET | `/phone/locations/{locationId}` | Get emergency service location details | `getLocation` | +| PATCH | `/phone/locations/{locationId}` | Update emergency service location | `updateLocation` | + +### External Contacts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/external_contacts` | List external contacts | `listExternalContacts` | +| POST | `/phone/external_contacts` | Add an external contact | `addExternalContact` | +| DELETE | `/phone/external_contacts/{externalContactId}` | Delete an external contact | `deleteAExternalContact` | +| GET | `/phone/external_contacts/{externalContactId}` | Get external contact details | `getAExternalContact` | +| PATCH | `/phone/external_contacts/{externalContactId}` | Update external contact | `updateExternalContact` | + +### Fax + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/fax/files` | Upload fax file | `UploadFaxFiles` | +| GET | `/phone/extension/{extensionId}/fax/logs` | Get extension's fax logs | `Getuser'sfaxlogs` | +| POST | `/phone/fax/documents` | Send fax | `SendEFax` | +| GET | `/phone/fax/logs` | Get account's fax logs | `GetAccount'sFaxLogs` | +| DELETE | `/phone/fax/logs/{faxLogId}` | Delete fax log | `DeleteFaxLog` | +| GET | `/phone/fax/logs/{faxLogId}` | Get fax log details | `GetFaxLogDetails` | +| PATCH | `/phone/fax/logs/{faxLogId}` | Update fax log read status | `UpdateFaxLogReadStatus` | +| GET | `/phone/fax/logs/{faxLogId}/file/{fileId}` | Download fax file | `Downloadfaxfile` | + +### Firmware Update Rules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/firmware_update_rules` | List firmware update rules | `ListFirmwareRules` | +| POST | `/phone/firmware_update_rules` | Add a firmware update rule | `AddFirmwareRule` | +| DELETE | `/phone/firmware_update_rules/{ruleId}` | Delete firmware update rule | `DeleteFirmwareUpdateRule` | +| GET | `/phone/firmware_update_rules/{ruleId}` | Get firmware update rule information | `GetFirmwareRuleDetail` | +| PATCH | `/phone/firmware_update_rules/{ruleId}` | Update firmware update rule | `UpdateFirmwareRule` | +| GET | `/phone/firmwares` | List updatable firmwares | `ListFirmwares` | + +### Group Call Pickup + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/group_call_pickup` | List group call pickup objects | `listGCP` | +| POST | `/phone/group_call_pickup` | Add a group call pickup object | `addGCP` | +| DELETE | `/phone/group_call_pickup/{groupId}` | Delete group call pickup objects | `deleteGCP` | +| GET | `/phone/group_call_pickup/{groupId}` | Get call pickup group by ID | `GetGCP` | +| PATCH | `/phone/group_call_pickup/{groupId}` | Update the group call pickup information | `updateGCP` | +| GET | `/phone/group_call_pickup/{groupId}/members` | List call pickup group members | `listGCPMembers` | +| POST | `/phone/group_call_pickup/{groupId}/members` | Add members to a call pickup group | `addGCPMembers` | +| DELETE | `/phone/group_call_pickup/{groupId}/members/{extensionId}` | Remove members from call pickup group | `removeGCPMembers` | + +### Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/groups/{groupId}/policies/{policyType}` | Get group policy details | `GetGroupPolicyDetails` | +| PATCH | `/phone/groups/{groupId}/policies/{policyType}` | Update group policy | `updateGroupPolicy` | +| GET | `/phone/groups/{groupId}/settings` | Get group phone settings | `getGroupPhoneSettings` | + +### Inbound Blocked List + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/extension/{extensionId}/inbound_blocked/rules` | Delete an extension's inbound block rule | `DeleteExtensiontLevelInboundBlockRules` | +| GET | `/phone/extension/{extensionId}/inbound_blocked/rules` | List an extension's inbound block rules | `ListExtensionLevelInboundBlockRules` | +| POST | `/phone/extension/{extensionId}/inbound_blocked/rules` | Add an extension's inbound block rule | `AddExtensiontLevelInboundBlockRules` | +| DELETE | `/phone/inbound_blocked/extension_rules/statistics` | Delete an account's inbound blocked statistics | `DeleteAccountLevelInboundBlockedStatistics` | +| GET | `/phone/inbound_blocked/extension_rules/statistics` | List an account's inbound blocked statistics | `ListAccountLevelInboundBlockedStatistics` | +| PATCH | `/phone/inbound_blocked/extension_rules/statistics/blocked_for_all` | Mark a phone number as blocked for all extensions | `MarkPhoneNumberAsBlockedForAllExtensions` | +| DELETE | `/phone/inbound_blocked/rules` | Delete an account's inbound block rule | `DeleteAccountLevelInboundBlockRules` | +| GET | `/phone/inbound_blocked/rules` | List an account's inbound block rules | `ListAccountLevelInboundBlockRules` | +| POST | `/phone/inbound_blocked/rules` | Add an account's inbound block rule | `AddAccountLevelInboundBlockRules` | +| PATCH | `/phone/inbound_blocked/rules/{blockedRuleId}` | Update an account's inbound block rule | `UpdateAccountLevelInboundBlockRule` | + +### IVR + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/auto_receptionists/{autoReceptionistId}/ivr` | Get auto receptionist IVR | `getAutoReceptionistIVR` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/ivr` | Update auto receptionist IVR | `updateAutoReceptionistIVR` | + +### Line Keys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/extension/{extensionId}/line_keys` | Get line key position and settings information | `listLineKeySetting` | +| PATCH | `/phone/extension/{extensionId}/line_keys` | Batch update line key position and settings information | `BatchUpdateLineKeySetting` | +| DELETE | `/phone/extension/{extensionId}/line_keys/{lineKeyId}` | Delete a line key setting. | `DeleteLineKey` | + +### Monitoring Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/monitoring_groups` | Get a list of monitoring groups on an account | `listMonitoringGroup` | +| POST | `/phone/monitoring_groups` | Create a monitoring group | `createMonitoringGroup` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}` | Delete a monitoring group | `deleteMonitoringGroup` | +| GET | `/phone/monitoring_groups/{monitoringGroupId}` | Get monitoring group by ID | `getMonitoringGroupById` | +| PATCH | `/phone/monitoring_groups/{monitoringGroupId}` | Update a monitoring group | `updateMonitoringGroup` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Remove all monitors or monitored members from a monitoring group | `removeMembers` | +| GET | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Get members of a monitoring group | `listMembers` | +| POST | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Add members to a monitoring group | `addMembers` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members/{memberExtensionId}` | Remove a member from a monitoring group | `removeMember` | + +### Outbound Calling + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/common_areas/{commonAreaId}/outbound_calling/countries_regions` | Get common area level outbound calling countries and regions | `GetCommonAreaOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/common_areas/{commonAreaId}/outbound_calling/countries_regions` | Update common area level outbound calling countries or regions | `UpdateCommonAreaOutboundCallingCountriesOrRegions` | +| GET | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules` | List common area level outbound calling exception rules | `listCommonAreaOutboundCallingExceptionRule` | +| POST | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules` | Add common area level outbound calling exception rule | `AddCommonAreaOutboundCallingExceptionRule` | +| DELETE | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete common area level outbound calling exception rule | `deleteCommonAreaOutboundCallingExceptionRule` | +| PATCH | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update common area level outbound calling exception rule | `UpdateCommonAreaOutboundCallingExceptionRule` | +| GET | `/phone/outbound_calling/countries_regions` | Get account level outbound calling countries and regions | `GetAccountOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/outbound_calling/countries_regions` | Update account level outbound calling countries or regions | `UpdateAccountOutboundCallingCountriesOrRegions` | +| GET | `/phone/outbound_calling/exception_rules` | List account level outbound calling exception rules | `listAccountOutboundCallingExceptionRule` | +| POST | `/phone/outbound_calling/exception_rules` | Add account level outbound calling exception rule | `AddAccountOutboundCallingExceptionRule` | +| DELETE | `/phone/outbound_calling/exception_rules/{exceptionRuleId}` | Delete account level outbound calling exception rule | `deleteAccountOutboundCallingExceptionRule` | +| PATCH | `/phone/outbound_calling/exception_rules/{exceptionRuleId}` | Update account level outbound calling exception rule | `UpdateAccountOutboundCallingExceptionRule` | +| GET | `/phone/sites/{siteId}/outbound_calling/countries_regions` | Get site level outbound calling countries and regions | `GetSiteOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/sites/{siteId}/outbound_calling/countries_regions` | Update site level outbound calling countries or regions | `UpdateSiteOutboundCallingCountriesOrRegions` | +| GET | `/phone/sites/{siteId}/outbound_calling/exception_rules` | List site level outbound calling exception rules | `listSiteOutboundCallingExceptionRule` | +| POST | `/phone/sites/{siteId}/outbound_calling/exception_rules` | Add site level outbound calling exception rule | `AddSiteOutboundCallingExceptionRule` | +| DELETE | `/phone/sites/{siteId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete site level outbound calling exception rule | `deleteSiteOutboundCallingExceptionRule` | +| PATCH | `/phone/sites/{siteId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update site level outbound calling exception rule | `UpdateSiteOutboundCallingExceptionRule` | +| GET | `/phone/users/{userId}/outbound_calling/countries_regions` | Get user level outbound calling countries and regions | `GetUserOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/users/{userId}/outbound_calling/countries_regions` | Update user level outbound calling countries or regions | `UpdateUserOutboundCallingCountriesOrRegions` | +| GET | `/phone/users/{userId}/outbound_calling/exception_rules` | List user level outbound calling exception rules | `listUserOutboundCallingExceptionRule` | +| POST | `/phone/users/{userId}/outbound_calling/exception_rules` | Add user level outbound calling exception rule | `AddUserOutboundCallingExceptionRule` | +| DELETE | `/phone/users/{userId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete user level outbound calling exception rule | `deleteUserOutboundCallingExceptionRule` | +| PATCH | `/phone/users/{userId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update user level outbound calling exception rule | `UpdateUserOutboundCallingExceptionRule` | + +### Phone Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/devices` | List devices | `listPhoneDevices` | +| POST | `/phone/devices` | Add a device | `addPhoneDevice` | +| POST | `/phone/devices/sync` | Sync deskphones | `syncPhoneDevice` | +| DELETE | `/phone/devices/{deviceId}` | Delete a device | `deleteADevice` | +| GET | `/phone/devices/{deviceId}` | Get device details | `getADevice` | +| PATCH | `/phone/devices/{deviceId}` | Update a device | `updateADevice` | +| POST | `/phone/devices/{deviceId}/extensions` | Assign an entity to a device | `addExtensionsToADevice` | +| DELETE | `/phone/devices/{deviceId}/extensions/{extensionId}` | Unassign an entity from the device | `deleteExtensionFromADevice` | +| PUT | `/phone/devices/{deviceId}/provision_templates` | Update provision template of a device | `updateProvisionTemplateToDevice` | +| POST | `/phone/devices/{deviceId}/reboot` | Reboot a desk phone | `rebootPhoneDevice` | +| GET | `/phone/smartphones` | List Smartphones | `ListSmartphones` | + +### Phone Numbers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/byoc_numbers` | Add BYOC phone numbers | `addBYOCNumber` | +| DELETE | `/phone/numbers` | Delete unassigned phone numbers | `deleteUnassignedPhoneNumbers` | +| GET | `/phone/numbers` | List phone numbers | `listAccountPhoneNumbers` | +| PATCH | `/phone/numbers/sites/{siteId}` | Update a site's unassigned phone numbers | `updateSiteForUnassignedPhoneNumbers` | +| GET | `/phone/numbers/{phoneNumberId}` | Get a phone number | `getPhoneNumberDetails` | +| PATCH | `/phone/numbers/{phoneNumberId}` | Update a phone number | `updatePhoneNumberDetails` | +| POST | `/phone/users/{userId}/phone_numbers` | Assign a phone number to a user | `assignPhoneNumber` | +| DELETE | `/phone/users/{userId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `UnassignPhoneNumber` | + +### Phone Plans + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/calling_plans` | List calling plans | `listCallingPlans` | +| GET | `/phone/plans` | List plan information | `listPhonePlans` | + +### Phone Roles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/roles` | List phone roles | `ListPhoneRoles` | +| POST | `/phone/roles` | Duplicate a phone role | `DuplicatePhoneRole` | +| DELETE | `/phone/roles/{roleId}` | Delete a phone role | `DeletePhoneRole` | +| GET | `/phone/roles/{roleId}` | Get role information | `getRoleInformation` | +| PATCH | `/phone/roles/{roleId}` | Update a phone role | `UpdatePhoneRole` | +| DELETE | `/phone/roles/{roleId}/members` | Delete members in a role | `DelRoleMembers` | +| GET | `/phone/roles/{roleId}/members` | List members in a role | `ListRoleMembers` | +| POST | `/phone/roles/{roleId}/members` | Add members to roles | `AddRoleMembers` | +| DELETE | `/phone/roles/{roleId}/targets` | Delete phone role targets | `DeletePhoneRoleTargets` | +| GET | `/phone/roles/{roleId}/targets` | List phone role targets | `ListPhoneRoleTargets` | +| POST | `/phone/roles/{roleId}/targets` | Add phone role targets | `AddPhoneRoleTargets` | + +### Private Directory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/private_directory/members` | List private directory members | `listPrivateDirectoryMembers` | +| POST | `/phone/private_directory/members` | Add members to a private directory | `addMembersToAPrivateDirectory` | +| DELETE | `/phone/private_directory/members/{extensionId}` | Remove a member from a private directory | `removeAMemberFromAPrivateDirectory` | +| PATCH | `/phone/private_directory/members/{extensionId}` | Update a private directory member | `updateAPrivateDirectoryMember` | + +### Provider Exchange + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/carrier_peering/numbers` | List carrier peering phone numbers. | `listCarrierPeeringPhoneNumbers` | +| DELETE | `/phone/peering/numbers` | Remove peering phone numbers | `deletePeeringPhoneNumbers` | +| GET | `/phone/peering/numbers` | List peering phone numbers | `listPeeringPhoneNumbers` | +| PATCH | `/phone/peering/numbers` | Update peering phone numbers | `updatePeeringPhoneNumbers` | +| POST | `/phone/peering/numbers` | Add peering phone numbers | `addPeeringPhoneNumbers` | + +### Provision Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/provision_templates` | List provision templates | `listAccountProvisionTemplate` | +| POST | `/phone/provision_templates` | Add a provision template | `addProvisionTemplate` | +| DELETE | `/phone/provision_templates/{templateId}` | Delete a provision template | `deleteProvisionTemplate` | +| GET | `/phone/provision_templates/{templateId}` | Get a provision template | `GetProvisionTemplate` | +| PATCH | `/phone/provision_templates/{templateId}` | Update a provision template | `updateProvisionTemplate` | + +### Recordings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_logs/{id}/recordings` | Get recording by call ID | `getPhoneRecordingsByCallIdOrCallLogId` | +| GET | `/phone/recording/download/{fileId}` | Download a phone recording | `phoneDownloadRecordingFile` | +| GET | `/phone/recording_transcript/download/{recordingId}` | Download a phone recording transcript | `phoneDownloadRecordingTranscript` | +| GET | `/phone/recordings` | Get call recordings | `getPhoneRecordings` | +| DELETE | `/phone/recordings/{recordingId}` | Delete a call recording | `deleteCallRecording` | +| PATCH | `/phone/recordings/{recordingId}` | Update auto delete field | `UpdateAutoDeleteField` | +| PUT | `/phone/recordings/{recordingId}/status` | Update Recording Status | `UpdateRecordingStatus` | +| GET | `/phone/users/{userId}/recordings` | Get user's recordings | `phoneUserRecordings` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/reports/call_charges` | Get call charges usage report | `GetCallChargesUsageReport` | +| GET | `/phone/reports/fax_charges` | Get fax charges usage report | `Getfaxchargesusagereport` | +| GET | `/phone/reports/operationlogs` | Get operation logs report | `getPSOperationLogs` | +| GET | `/phone/reports/sms_charges` | Get SMS/MMS charges usage report | `GetSMSChargesUsageReport` | + +### Routing Rules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/routing_rules` | List directory backup routing rules | `listRoutingRule` | +| POST | `/phone/routing_rules` | Add directory backup routing rule | `addRoutingRule` | +| DELETE | `/phone/routing_rules/{routingRuleId}` | Delete directory backup routing rule | `deleteRoutingRule` | +| GET | `/phone/routing_rules/{routingRuleId}` | Get directory backup routing rule | `getRoutingRule` | +| PATCH | `/phone/routing_rules/{routingRuleId}` | Update directory backup routing rule | `updateRoutingRule` | + +### Setting Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/setting_templates` | List setting templates | `listSettingTemplates` | +| POST | `/phone/setting_templates` | Add a setting template | `addSettingTemplate` | +| GET | `/phone/setting_templates/{templateId}` | Get setting template details | `getSettingTemplate` | +| PATCH | `/phone/setting_templates/{templateId}` | Update a setting template | `updateSettingTemplate` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/policies/{policyType}` | Get account policy details | `GetAccountPolicyDetails` | +| PATCH | `/phone/policies/{policyType}` | Update account policy | `updateAccountPolicy` | +| GET | `/phone/ported_numbers/orders` | List ported numbers | `listPortedNumbers` | +| GET | `/phone/ported_numbers/orders/{orderId}` | Get ported numbers details | `getPortedNumbersDetails` | +| GET | `/phone/settings` | Get phone account settings | `phoneSetting` | +| PATCH | `/phone/settings` | Update phone account settings | `updatePhoneSettings` | +| GET | `/phone/sip_groups` | List SIP groups | `listSipGroups` | +| GET | `/phone/sip_trunk/trunks` | List BYOC SIP trunks | `listBYOCSIPTrunk` | + +### Shared Line Appearance + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/shared_line_appearances` | List shared line appearances | `listSharedLineAppearances` | + +### Shared Line Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/shared_line_groups` | List shared line groups | `listSharedLineGroups` | +| POST | `/phone/shared_line_groups` | Create a shared line group | `createASharedLineGroup` | +| GET | `/phone/shared_line_groups/{sharedLineGroupId}` | Get a shared line group | `getASharedLineGroup` | +| GET | `/phone/shared_line_groups/{sharedLineGroupId}/policies` | Get a shared line group policy | `getSharedLineGroupPolicy` | +| PATCH | `/phone/shared_line_groups/{sharedLineGroupId}/policies` | Update a shared line group policy | `updateSharedLineGroupPolicy` | +| DELETE | `/phone/shared_line_groups/{slgId}` | Delete a shared line group | `deleteASharedLineGroup` | +| PATCH | `/phone/shared_line_groups/{slgId}` | Update a shared line group | `updateASharedLineGroup` | +| DELETE | `/phone/shared_line_groups/{slgId}/members` | Unassign members from a shared line group | `deleteMembersOfSLG` | +| POST | `/phone/shared_line_groups/{slgId}/members` | Add members to a shared line group | `addMembersToSharedLineGroup` | +| DELETE | `/phone/shared_line_groups/{slgId}/members/{memberId}` | Unassign a member from a shared line group | `deleteAMemberSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/phone_numbers` | Unassign all phone numbers | `deletePhoneNumbersSLG` | +| POST | `/phone/shared_line_groups/{slgId}/phone_numbers` | Assign phone numbers | `assignPhoneNumbersSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `deleteAPhoneNumberSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Delete an SLG policy setting | `removeSLGPolicySubSetting` | +| PATCH | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Update an SLG policy setting | `updateSLGPolicySubSetting` | +| POST | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Add a policy setting to a shared line group | `addSLGPolicySubSetting` | + +### Sites + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sites` | List phone sites | `listPhoneSites` | +| POST | `/phone/sites` | Create a phone site | `createPhoneSite` | +| DELETE | `/phone/sites/{siteId}` | Delete a phone site | `deletePhoneSite` | +| GET | `/phone/sites/{siteId}` | Get phone site details | `getASite` | +| PATCH | `/phone/sites/{siteId}` | Update phone site details | `updateSiteDetails` | +| DELETE | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | Remove customized outbound caller ID phone numbers | `deleteSiteOutboundCallerNumbers` | +| GET | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | List customized outbound caller ID phone numbers | `listSiteCustomizeOutboundCallerNumbers` | +| POST | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | Add customized outbound caller ID phone numbers | `addSiteOutboundCallerNumbers` | +| DELETE | `/phone/sites/{siteId}/settings/{settingType}` | Delete a site setting | `deleteSiteSetting` | +| GET | `/phone/sites/{siteId}/settings/{settingType}` | Get a phone site setting | `getSiteSettingForType` | +| PATCH | `/phone/sites/{siteId}/settings/{settingType}` | Update the site setting | `updateSiteSetting` | +| POST | `/phone/sites/{siteId}/settings/{settingType}` | Add a site setting | `addSiteSetting` | + +### SMS + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/sms/messages` | Post SMS message | `postSmsMessage` | +| GET | `/phone/sms/sessions` | Get account's SMS sessions | `accountSmsSession` | +| GET | `/phone/sms/sessions/{sessionId}` | Get SMS session details | `smsSessionDetails` | +| GET | `/phone/sms/sessions/{sessionId}/messages/{messageId}` | Get SMS by message ID | `smsByMessageId` | +| GET | `/phone/sms/sessions/{sessionId}/sync` | Sync SMS by session ID | `smsSessionSync` | +| GET | `/phone/users/{userId}/sms/sessions` | Get user's SMS sessions | `userSmsSession` | +| GET | `/phone/users/{userId}/sms/sessions/sync` | List user's SMS sessions in descending order | `GetSmsSessions` | + +### SMS Campaign + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sms_campaigns` | List SMS campaigns | `listAccountSMSCampaigns` | +| GET | `/phone/sms_campaigns/{smsCampaignId}` | Get an SMS campaign | `GetSMSCampaign` | +| POST | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers` | Assign a phone number to SMS campaign | `assignCampaignPhoneNumbers` | +| GET | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/opt_status` | List opt statuses of phone numbers assigned to SMS campaign | `getNumberCampaignOptStatus` | +| PATCH | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/opt_status` | Update opt statuses of phone numbers assigned to SMS campaign | `updateNumberCampaignOptStatus` | +| DELETE | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unassignCampaignPhoneNumber` | +| GET | `/phone/user/{userId}/sms_campaigns/phone_numbers/opt_status` | List user's opt statuses of phone numbers | `getUserNumberCampaignOptStatus` | + +### SMS Consent + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sms_consent/{consentId}/phone_numbers/opt_status` | List opt statuses of phone numbers assigned to SMS consent | `getNumberConsentOptStatus` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/users` | List phone users | `listPhoneUsers` | +| POST | `/phone/users/batch` | Batch add users | `batchAddUsers` | +| PUT | `/phone/users/batch` | Update multiple users' properties in batch | `updateUsersPropertiesInBatch` | +| GET | `/phone/users/{userId}` | Get a user's profile | `phoneUser` | +| PATCH | `/phone/users/{userId}` | Update a user's profile | `updateUserProfile` | +| POST | `/phone/users/{userId}/calling_plans` | Assign calling plan to a user | `assignCallingPlan` | +| PUT | `/phone/users/{userId}/calling_plans` | Update user's calling plan | `updateCallingPlan` | +| DELETE | `/phone/users/{userId}/calling_plans/{planType}` | Unassign user's calling plan | `unassignCallingPlan` | +| DELETE | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | Remove users' customized outbound caller ID phone numbers | `deleteUserOutboundCallerNumbers` | +| GET | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | List users' phone numbers for a customized outbound caller ID | `listUserCustomizeOutboundCallerNumbers` | +| POST | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | Add phone numbers for users' customized outbound caller ID | `addUserOutboundCallerNumbers` | +| GET | `/phone/users/{userId}/policies/{policyType}` | Get user policy details | `GetUserPolicyDetails` | +| PATCH | `/phone/users/{userId}/policies/{policyType}` | Update user policy | `updateUserPolicy` | +| GET | `/phone/users/{userId}/settings` | Get a user's profile settings | `phoneUserSettings` | +| PATCH | `/phone/users/{userId}/settings` | Update a user's profile settings | `updateUserSettings` | +| DELETE | `/phone/users/{userId}/settings/{settingType}` | Delete a user's shared access setting | `deleteUserSetting` | +| PATCH | `/phone/users/{userId}/settings/{settingType}` | Update a user's shared access setting | `updateUserSetting` | +| POST | `/phone/users/{userId}/settings/{settingType}` | Add a user's shared access setting | `addUserSetting` | + +### Voicemails + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/users/{userId}/call_logs/{id}/voice_mail` | Get user voicemail details from a call log | `getVoicemailDetailsByCallIdOrCallLogId` | +| GET | `/phone/users/{userId}/voice_mails` | Get user's voicemails | `phoneUserVoiceMails` | +| GET | `/phone/voice_mails` | Get account voicemails | `accountVoiceMails` | +| GET | `/phone/voice_mails/download/{fileId}` | Download a phone voicemail | `phoneDownloadVoicemailFile` | +| DELETE | `/phone/voice_mails/{voicemailId}` | Delete a voicemail | `deleteVoicemail` | +| GET | `/phone/voice_mails/{voicemailId}` | Get voicemail details | `getVoicemailDetails` | +| PATCH | `/phone/voice_mails/{voicemailId}` | Update Voicemail Read Status | `updateVoicemailReadStatus` | + +### Zoom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/rooms` | List Zoom Rooms under Zoom Phone license | `listZoomRooms` | +| POST | `/phone/rooms` | Add a Zoom Room to a Zoom Phone | `addZoomRoom` | +| GET | `/phone/rooms/unassigned` | List Zoom Rooms without Zoom Phone assignment | `listUnassignedZoomRooms` | +| DELETE | `/phone/rooms/{roomId}` | Remove a Zoom Room from a ZP account | `RemoveZoomRoom` | +| GET | `/phone/rooms/{roomId}` | Get a Zoom Room under Zoom Phone license | `getZoomRoom` | +| PATCH | `/phone/rooms/{roomId}` | Update a Zoom Room under Zoom Phone license | `updateZoomRoom` | +| POST | `/phone/rooms/{roomId}/calling_plans` | Assign calling plans to a Zoom Room | `assignCallingPlanToRoom` | +| DELETE | `/phone/rooms/{roomId}/calling_plans/{type}` | Remove a calling plan from a Zoom Room | `unassignCallingPlanFromRoom` | +| POST | `/phone/rooms/{roomId}/phone_numbers` | Assign phone numbers to a Zoom Room | `assignPhoneNumberToZoomRoom` | +| DELETE | `/phone/rooms/{roomId}/phone_numbers/{phoneNumberId}` | Remove a phone number from a Zoom Room | `UnassignPhoneNumberFromZoomRoom` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/qss.md b/partner-built/zoom-plugin/skills/rest-api/references/qss.md new file mode 100644 index 00000000..62eae230 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/qss.md @@ -0,0 +1,45 @@ +# Zoom QSS API + +Authoritative endpoint inventory for QSS. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/qss/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 3 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Dashboards | 2 | +| Video SDK Sessions | 1 | + +## Endpoints by Tag + +### Dashboards + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/metrics/meetings/{meetingId}/participants/qos_summary` | List meeting participants QoS Summary | `dashboardMeetingParticipantsQOSSummary` | +| GET | `/metrics/webinars/{webinarId}/participants/qos_summary` | List webinar participants QoS Summary | `dashboardWebinarParticipantsQOSSummary` | + +### Video SDK Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/sessions/{sessionId}/users/qos_summary` | List session users QoS Summary | `sessionUsersQOSSummary` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/quality-management.md b/partner-built/zoom-plugin/skills/rest-api/references/quality-management.md new file mode 100644 index 00000000..c214e6cc --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/quality-management.md @@ -0,0 +1,48 @@ +# Zoom Quality Management API + +Authoritative endpoint inventory for Quality Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/quality-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 6 | +| Path templates | 5 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Evaluations | 3 | +| Interactions | 3 | + +## Endpoints by Tag + +### Evaluations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/qm/automated_evaluations` | List automated evaluations | `ListAutomatedEvaluations` | +| GET | `/qm/evaluation` | List evaluations | `Listevaluations` | +| GET | `/qm/evaluation/{evaluationId}` | View evaluation detail | `EvaluationDetail` | + +### Interactions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/qm/interactions` | List interactions | `ListInteractions` | +| POST | `/qm/interactions` | Add an interaction | `Addinteraction` | +| GET | `/qm/interactions/{interactionId}` | View interaction detail | `InteractionDetail` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/rate-limits.md b/partner-built/zoom-plugin/skills/rest-api/references/rate-limits.md new file mode 100644 index 00000000..fb18b1ab --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/rate-limits.md @@ -0,0 +1,386 @@ +# REST API - Rate Limits + +Understanding and handling Zoom API rate limits for reliable integrations. + +## Overview + +Zoom APIs enforce rate limits to ensure fair usage. Limits vary by endpoint category, account type, and are shared across all apps on an account. + +## Rate Limit Categories + +### 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* | + +**\* Daily limits (shared):** +- **Pro**: 30,000/day (Heavy + Resource-Intensive combined) +- **Business+**: 60,000/day (Heavy + Resource-Intensive combined) + +### 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* | + +### 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** + +## Endpoint Category Examples + +| Light | Medium | Heavy | +|-------|--------|-------| +| Add Meeting Registrant | Create Meeting | Get Daily Usage Report | +| Get A Meeting | Get Past Meeting Participants | List Devices | +| Get Meeting Recordings | List All Recordings | | +| Update A Meeting | List Meetings | | +| Delete Meeting Recordings | List Webinars | | + +## Special Per-User Limits + +### Meeting/Webinar Operations + +| 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 | + +**Note:** The 100/day limit applies to all Meeting/Webinar IDs hosted by a specific user. + +### Lock-Key Limits (Concurrent Operations) + +Zoom enforces lock-key limits for user 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 Response:** +```json +{ + "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 headers: + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Category` | Light, Medium, Heavy, or Resource-intensive | +| `X-RateLimit-Type` | `QPS` (per-second) or `Daily-limit` | +| `X-RateLimit-Limit` | Maximum requests in current window | +| `X-RateLimit-Remaining` | Requests remaining in current window | + +### On Per-Second/Minute Limit Hit + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Reset` | Unix timestamp when limit resets | + +### On Daily Limit Hit + +| Header | Description | +|--------|-------------| +| `Retry-After` | ISO8601 datetime when you can retry | + +### Example Headers + +**Normal response:** +``` +X-RateLimit-Category: Medium +X-RateLimit-Type: QPS +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 55 +``` + +**Rate limited (per-second):** +``` +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: 1705312800 +``` + +**Rate limited (daily):** +``` +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:00Z +``` + +## Error Responses + +### Per-Second Limit + +```json +{ + "code": 429, + "message": "You have reached the maximum per-second rate limit for this API. Try again later." +} +``` + +### Daily Limit + +```json +{ + "code": 429, + "message": "You have reached the maximum daily rate limit for this API. Refer to the response header for details on when you can make another request." +} +``` + +## Handling Rate Limits + +### Basic Retry with Backoff + +```javascript +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 Retry-After (daily limit) + const retryAfter = response.headers.get('Retry-After'); + if (retryAfter) { + const retryDate = new Date(retryAfter); + const waitMs = retryDate - Date.now(); + console.log(`Daily limit hit. Retry after: ${retryAfter}`); + await sleep(waitMs); + continue; + } + + // Per-second limit - use exponential backoff + const delay = Math.pow(2, attempt) * 1000; + const jitter = delay * 0.2 * Math.random(); // 20% jitter + console.log(`Rate limited. Retrying in ${delay + jitter}ms`); + await sleep(delay + jitter); + continue; + } + + return response; + } + + throw new Error('Max retries exceeded'); +} +``` + +### Monitor Rate Limit Headers + +```javascript +async function callAPIWithMonitoring(url, options) { + const response = await fetch(url, options); + + const remaining = parseInt(response.headers.get('X-RateLimit-Remaining')); + const limit = parseInt(response.headers.get('X-RateLimit-Limit')); + const category = response.headers.get('X-RateLimit-Category'); + const type = response.headers.get('X-RateLimit-Type'); + + console.log(`[${category}/${type}] ${remaining}/${limit} remaining`); + + // Proactive throttling + if (remaining < limit * 0.1) { // Less than 10% remaining + console.warn('Approaching rate limit - throttling requests'); + await sleep(1000); // Slow down + } + + return response; +} +``` + +### Request Queue Pattern + +For high-volume applications: + +```javascript +class RateLimitedQueue { + constructor(maxConcurrent = 10, minDelayMs = 100) { + this.queue = []; + this.running = 0; + this.maxConcurrent = maxConcurrent; + this.minDelayMs = minDelayMs; + } + + async add(requestFn) { + return new Promise((resolve, reject) => { + this.queue.push({ requestFn, resolve, reject }); + this.process(); + }); + } + + async process() { + if (this.running >= this.maxConcurrent || this.queue.length === 0) { + return; + } + + const { requestFn, resolve, reject } = this.queue.shift(); + this.running++; + + try { + const result = await requestFn(); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.running--; + await sleep(this.minDelayMs); + this.process(); + } + } +} + +// Usage +const queue = new RateLimitedQueue(10, 100); // 10 concurrent, 100ms min delay + +const results = await Promise.all([ + queue.add(() => fetch('/api/users/1')), + queue.add(() => fetch('/api/users/2')), + queue.add(() => fetch('/api/users/3')), + // ... more requests +]); +``` + +## Best Practices + +### 1. Cache GET Responses + +```javascript +const cache = new Map(); +const CACHE_TTL = 60000; // 1 minute + +async function cachedGet(url, options) { + const cached = cache.get(url); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + + const response = await fetch(url, options); + const data = await response.json(); + + cache.set(url, { data, timestamp: Date.now() }); + return data; +} +``` + +### 2. Use Webhooks Instead of Polling + +Instead of polling for changes: +```javascript +// DON'T: Poll every minute +setInterval(async () => { + const meetings = await getMeetings(); // Uses API quota +}, 60000); +``` + +Use webhooks: +```javascript +// DO: Receive webhook events +app.post('/webhook', (req, res) => { + const event = req.body; + if (event.event === 'meeting.started') { + handleMeetingStarted(event.payload); + } + res.status(200).send(); +}); +``` + +### 3. Batch Operations + +Use list endpoints with pagination instead of individual fetches: +```javascript +// DON'T: Fetch users one by one +for (const userId of userIds) { + const user = await getUser(userId); // N API calls +} + +// DO: Fetch users in batches +const users = await listUsers({ page_size: 300 }); // 1 API call +``` + +### 4. Use QSS for Quality Data + +For Quality of Service data, use QSS (push-based) instead of polling: +- QSS streams telemetry via webhooks/WebSocket +- Pushes data 4-6 times per minute +- Reduces need for Reports API polling + +### 5. Distribute Requests Over Time + +```javascript +// DON'T: Burst all requests at once +await Promise.all(users.map(u => updateUser(u))); // May hit rate limit + +// DO: Distribute over time +for (const user of users) { + await updateUser(user); + await sleep(100); // 100ms between requests +} +``` + +## Account Type Notes + +### Rate Limits Are Per-Account + +- Limits are shared by ALL users and ALL apps on the account +- Upgrading account increases limits for everyone +- Not per-app - one heavy app can impact others + +### Business+ Includes + +- Business +- Education +- Enterprise +- Partners + +### Video SDK Accounts + +| Plan | Uses Limits | +|------|-------------| +| Pay As You Go (Deprecated) | Pro | +| Annual Prepay Monthly Usage | Pro | +| All other plans | Business+ | + +## Event Subscription Limits + +- **Maximum 10 event subscriptions** per application +- **No limit** on events per subscription +- Event subscription API has **Heavy** rate limit +- WebSocket: Only 1 subscription connection at a time (new connection closes previous) + +## 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 - use different host | +| Concurrent DELETE errors | Serialize DELETE operations on same user | +| Daily limit hit unexpectedly | Heavy + Resource-Intensive share quota | + +## Resources + +- **Rate limits docs**: https://developers.zoom.us/docs/api/rest/rate-limits/ +- **QSS docs**: https://developers.zoom.us/docs/api/rest/qss-api/ +- **Webhooks docs**: https://developers.zoom.us/docs/api/rest/webhook-reference/ diff --git a/partner-built/zoom-plugin/skills/rest-api/references/recordings.md b/partner-built/zoom-plugin/skills/rest-api/references/recordings.md new file mode 100644 index 00000000..19c8ce18 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/recordings.md @@ -0,0 +1,70 @@ +# REST API - Recordings + +Cloud recording management endpoints. + +## Overview + +Access and manage Zoom cloud recordings. + +## Endpoints + +### List User Recordings + +```bash +GET /users/{userId}/recordings +``` + +Query parameters: +- `from` - Start date (YYYY-MM-DD) +- `to` - End date (YYYY-MM-DD) + +### Get Meeting Recordings + +```bash +GET /meetings/{meetingId}/recordings +``` + +### Delete Recordings + +```bash +DELETE /meetings/{meetingId}/recordings +``` + +### Delete Single Recording File + +```bash +DELETE /meetings/{meetingId}/recordings/{recordingId} +``` + +## Recording Files + +Response includes multiple file types: + +| Type | Description | +|------|-------------| +| `shared_screen_with_speaker_view` | Screen share + speaker | +| `shared_screen_with_gallery_view` | Screen share + gallery | +| `active_speaker` | Speaker view only | +| `gallery_view` | Gallery view only | +| `audio_only` | Audio file (M4A) | +| `chat_file` | Chat transcript | +| `timeline` | Meeting timeline | +| `audio_transcript` | VTT transcript | + +## Download Recordings + +```bash +# Get download URL from recording files +GET {download_url}?access_token={token} +``` + +**Note:** Download URLs require authentication. + +## Required Scopes + +- `recording:read` - View/download recordings +- `recording:write` - Delete recordings + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording diff --git a/partner-built/zoom-plugin/skills/rest-api/references/reports.md b/partner-built/zoom-plugin/skills/rest-api/references/reports.md new file mode 100644 index 00000000..013127a8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/reports.md @@ -0,0 +1,86 @@ +# REST API - Reports + +Usage reports and analytics endpoints. + +## Overview + +Access meeting statistics, usage reports, and analytics data. + +## Endpoints + +### Daily Usage Report + +```bash +GET /report/daily +``` + +Query parameters: +- `year` - Year (required) +- `month` - Month (required) + +### Meeting Participants Report + +```bash +GET /report/meetings/{meetingId}/participants +``` + +### Meeting Details Report + +```bash +GET /report/meetings/{meetingId} +``` + +### Webinar Participants Report + +```bash +GET /report/webinars/{webinarId}/participants +``` + +### Active/Inactive Host Report + +```bash +GET /report/users +``` + +## Response Data + +### Daily Usage + +```json +{ + "dates": [ + { + "date": "2024-01-15", + "new_users": 5, + "meetings": 25, + "participants": 150, + "meeting_minutes": 3600 + } + ] +} +``` + +### Meeting Participants + +```json +{ + "participants": [ + { + "id": "user_id", + "name": "User Name", + "user_email": "user@example.com", + "join_time": "2024-01-15T10:00:00Z", + "leave_time": "2024-01-15T11:00:00Z", + "duration": 3600 + } + ] +} +``` + +## Required Scopes + +- `report:read` - View reports + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports diff --git a/partner-built/zoom-plugin/skills/rest-api/references/revenue-accelerator.md b/partner-built/zoom-plugin/skills/rest-api/references/revenue-accelerator.md new file mode 100644 index 00000000..1d0d913c --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/revenue-accelerator.md @@ -0,0 +1,141 @@ +# Zoom Revenue Accelerator API + +Authoritative endpoint inventory for Revenue Accelerator. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/iq/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 75 | +| Path templates | 51 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Accounts | 2 | +| Conversations | 34 | +| CRM | 12 | +| Deals | 8 | +| ScheduleMeetings | 1 | +| Teams | 18 | + +## Endpoints by Tag + +### Accounts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/settings/indicators` | Get indicators settings [Deprecated] | `accountSettingsIndicatorsDeprecated` | +| GET | `/zra/settings/indicators` | Get indicators settings | `accountIndicatorsSettings` | + +### Conversations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/conversations` | List conversations [Deprecated] | `listConversationsDeprecated` | +| POST | `/iq/conversations` | Add conversation by file id or download url. [Deprecated] | `AddConversationByFileIdOrDownloadUrlDeprecated` | +| DELETE | `/iq/conversations/{conversationId}` | Delete conversation by conversation ID [Deprecated] | `deleteConversationDeprecated` | +| GET | `/iq/conversations/{conversationId}` | Get conversation information [Deprecated] | `getConversationInfoDeprecated` | +| GET | `/iq/conversations/{conversationId}/comments` | Get conversation comments [Deprecated] | `getConversationCommentsDeprecated` | +| POST | `/iq/conversations/{conversationId}/comments` | Add new comments to the conversation [Deprecated] | `addConversationCommentDeprecated` | +| DELETE | `/iq/conversations/{conversationId}/comments/{commentId}` | Delete conversation's comment [Deprecated] | `deleteConversationCommentDeprecated` | +| PATCH | `/iq/conversations/{conversationId}/comments/{commentId}` | Edit conversation comment [Deprecated] | `editConversationCommentDeprecated` | +| GET | `/iq/conversations/{conversationId}/content_analysis` | Get conversation content analysis [Deprecated] | `getConversationContentAnalysisDeprecated` | +| GET | `/iq/conversations/{conversationId}/interactions` | Get conversation interactions [Deprecated] | `getConversationInteractionsDeprecated` | +| GET | `/iq/conversations/{conversationId}/scorecards` | Get conversation scorecards [Deprecated] | `getConversationScorecardsDeprecated` | +| PATCH | `/iq/conversations/{conversationId}/update_host` | Update conversation host id to new host id by conversation id [Deprecated] | `UpdateconversationhostidtonewhostidbyconversationidDeprecated` | +| POST | `/iq/files` | Upload IQ file [Deprecated] | `UploadIQFileDeprecated` | +| POST | `/iq/files/multipart` | Upload iq multipart file. [Deprecated] | `UploadIqMultipartFileDeprecated` | +| POST | `/iq/files/multipart/upload_events` | Initiate and complete a multipart upload. [Deprecated] | `InitiateAndCompleteAMultipartUpload. [Deprecated]` | +| POST | `/iq/users/{userId}/conversations` | Add conversation by meeting record url or meeting UUID. [Deprecated] | `addConversationDeprecated` | +| GET | `/iq/users/{userId}/conversations/playlists` | Get a user's playlist [Deprecated] | `getUserPlaylistDeprecated` | +| GET | `/zra/conversations` | List conversations | `listAllConversations` | +| POST | `/zra/conversations` | Add conversation by file id or download url. | `AddConversationByFileIdOrDownloadUrl` | +| DELETE | `/zra/conversations/{conversationId}` | Delete conversation by conversation ID | `deleteConversationById` | +| GET | `/zra/conversations/{conversationId}` | Get conversation information | `getConversationDetail` | +| GET | `/zra/conversations/{conversationId}/comments` | Get conversation comments | `getConversationCommentsById` | +| POST | `/zra/conversations/{conversationId}/comments` | Add new comments to the conversation | `addConversationComments` | +| DELETE | `/zra/conversations/{conversationId}/comments/{commentId}` | Delete conversation's comment | `deleteConversationCommentById` | +| PATCH | `/zra/conversations/{conversationId}/comments/{commentId}` | Edit conversation comment | `editConversationCommentById` | +| GET | `/zra/conversations/{conversationId}/content_analysis` | Get conversation content analysis | `getConversationContentAnalysisById` | +| GET | `/zra/conversations/{conversationId}/interactions` | Get conversation interactions | `getConversationInteraction` | +| GET | `/zra/conversations/{conversationId}/scorecards` | Get conversation scorecards | `getConversationScorecardsById` | +| PATCH | `/zra/conversations/{conversationId}/update_host` | Update conversation host id | `UpdateConversationhostid2NewHostidByConversationId` | +| POST | `/zra/files` | Upload IQ file | `UploadZraFile` | +| POST | `/zra/files/multipart` | Upload iq multipart file. | `UploadZraMultipartFile.` | +| POST | `/zra/files/multipart/upload_events` | Initiate and complete a multipart upload. | `InitiateAndCompleteAMultipartUpload` | +| POST | `/zra/users/{userId}/conversations` | Add conversation by meeting record url or meeting UUID. | `addConversationByRecord` | +| GET | `/zra/users/{userId}/conversations/playlists` | Get a user's playlist | `getUserPlaylists` | + +### CRM + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zra/crm/accounts` | Get CRM accounts | `getCrmAccounts` | +| POST | `/zra/crm/accounts` | Bulk import CRM accounts | `bulkImportAccounts` | +| GET | `/zra/crm/contacts` | Get CRM contacts | `getCrmContacts` | +| POST | `/zra/crm/contacts` | Bulk import CRM contacts | `bulkImportContacts` | +| GET | `/zra/crm/deals` | Get CRM deals | `getCrmDeals` | +| POST | `/zra/crm/deals` | Bulk import CRM deals | `bulkImportDeals` | +| GET | `/zra/crm/leads` | Get CRM leads | `getCrmLeads` | +| POST | `/zra/crm/leads` | Bulk import CRM leads | `bulkImportLeads` | +| DELETE | `/zra/crm/settings` | Unregister CRM API connection | `unregisterCrm` | +| GET | `/zra/crm/settings` | Get current CRM API registration | `getCrmRegistration` | +| POST | `/zra/crm/settings` | Register CRM API connection | `registerCrm` | +| GET | `/zra/crm/tasks/{taskId}` | Poll async task result | `getAsyncTask` | + +### Deals + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/deals` | List deals [Deprecated] | `listDealsDeprecated` | +| GET | `/iq/deals/{dealId}` | Get deal information [Deprecated] | `getDealInfoDeprecated` | +| DELETE | `/iq/deals/{dealId}/activities` | Delete activity from the deal [Deprecated] | `DeleteActivityFromTheDealDeprecated` | +| GET | `/iq/deals/{dealId}/activities` | Get deal activities [Deprecated] | `getDealActivitiesDeprecated` | +| GET | `/zra/deals` | List deals | `listAllDeals` | +| GET | `/zra/deals/{dealId}` | Get deal information | `getDealDetail` | +| DELETE | `/zra/deals/{dealId}/activities` | Delete activity from the deal | `DeleteActivityFromDeal` | +| GET | `/zra/deals/{dealId}/activities` | Get deal activities | `geAllActivitiesFromDeal` | + +### ScheduleMeetings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zra/scheduled` | List scheduled meetings | `Listallscheduledmeetings` | + +### Teams + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/teams` | List Account Teams [Deprecated] | `ListAccountTeamsDeprecated` | +| POST | `/zra/team` | Create Team | `CreateTeam` | +| GET | `/zra/team/unassigned_team_users` | List Unassigned Team Users | `ListUnassignedTeamUsers` | +| DELETE | `/zra/team/{teamId}` | Delete Team | `DeleteTeam` | +| GET | `/zra/team/{teamId}` | Get Team Detail | `GetTeamDetail` | +| PATCH | `/zra/team/{teamId}` | Update Team name | `UpdateTeam` | +| DELETE | `/zra/team/{teamId}/access/granted-from` | Remove additional access from current team | `DeleteSharedFromTeams` | +| POST | `/zra/team/{teamId}/access/granted-from` | Grant additional access to current team | `AddSharedFromTeams` | +| DELETE | `/zra/team/{teamId}/access/granted-to` | Remove additional access from target teams | `DeleteSharedToTeams` | +| POST | `/zra/team/{teamId}/access/granted-to` | Grant additional access to target teams | `AddSharedToTeams` | +| PATCH | `/zra/team/{teamId}/parent_team` | Move team to new parent | `MoveTeam` | +| DELETE | `/zra/team/{teamId}/team_managers` | Unassign Team Managers | `UnassignTeamManagers` | +| GET | `/zra/team/{teamId}/team_managers` | List Team Managers | `ListTeamManagers` | +| POST | `/zra/team/{teamId}/team_managers` | Assign Team Managers | `AssignTeamManagers` | +| DELETE | `/zra/team/{teamId}/team_members` | Unassign Team Members | `UnassignTeamMembers` | +| GET | `/zra/team/{teamId}/team_members` | List Team Members | `ListTeamMembers` | +| POST | `/zra/team/{teamId}/team_members` | Assign Team Members | `AssignTeamMembers` | +| GET | `/zra/teams` | List Account Teams | `ListTeams` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/rooms.md b/partner-built/zoom-plugin/skills/rest-api/references/rooms.md new file mode 100644 index 00000000..e2eedbcb --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/rooms.md @@ -0,0 +1,211 @@ +# Zoom Rooms API + +Authoritative endpoint inventory for Rooms. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/rooms/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 121 | +| Path templates | 70 | +| Tags | 10 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Room Apps | 1 | +| Visitor Management | 6 | +| Workspaces | 33 | +| Zoom Rooms | 20 | +| Zoom Rooms Account | 4 | +| Zoom Rooms Calendar | 7 | +| Zoom Rooms Content | 31 | +| Zoom Rooms Devices | 2 | +| Zoom Rooms Location | 10 | +| Zoom Rooms Tags | 7 | + +## Endpoints by Tag + +### Room Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/rooms/controller/apps/config` | Config Zoom Room Controller Apps | `ConfigZoomRoomControllerApps` | + +### Visitor Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/visitor/invitation` | Get a list of visitors by location | `invitationList` | +| POST | `/visitor/invitation` | Send an invitation | `createInvitation` | +| DELETE | `/visitor/invitation/{invitationId}` | Delete an Invitation | `deleteInvitation` | +| GET | `/visitor/invitation/{invitationId}` | Invitation details by invitationID | `getInvitation` | +| PATCH | `/visitor/invitation/{invitationId}` | Update an invitation | `updateInvitation` | +| POST | `/visitor/invitation/{invitationId}/checkin` | Check in a visitor | `checkinVisitor` | + +### Workspaces + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workspaces` | List workspaces | `listWorkspaces` | +| POST | `/workspaces` | Create a workspace | `createWorkspace` | +| GET | `/workspaces/additional_informations` | List workspace additional information with time range | `Getaworkspaceadditionalenhancements` | +| GET | `/workspaces/assets` | Get all workspace assets | `Getallworkspaceassets` | +| POST | `/workspaces/assets` | Create a workspace asset | `AddaWorkspaceasset` | +| DELETE | `/workspaces/assets/{assetId}` | Delete a workspace asset | `DeleteaWorkspaceasset` | +| GET | `/workspaces/assets/{assetId}` | Get a workspace asset | `get_workspace_asset` | +| PATCH | `/workspaces/assets/{assetId}` | Edit a workspace asset | `PatchWorkspaceasset` | +| POST | `/workspaces/events` | Check in/out of a reservation | `reservationEvent` | +| POST | `/workspaces/floormap/files` | Add or Update a Workspace floor map | `AddOrUpdateAWorkspaceFloorMap` | +| GET | `/workspaces/locations/{locationId}/floor_map` | Get a workspace location floor map | `Getlocationfloormap` | +| GET | `/workspaces/released_workspaces_by_timeout` | List released workspaces by timeout | `getWorksapceReservationReleaseInof` | +| PATCH | `/workspaces/settings` | Update workspace settings | `updateWorkspaceSettings` | +| DELETE | `/workspaces/settings/photos` | Delete workspace photos | `DeleteaWorkspacephoto` | +| POST | `/workspaces/settings/photos` | Add a workspace photo | `AddaWorkspacephoto` | +| GET | `/workspaces/usage` | Get a location's hot desk usage | `getHotDeskUsage` | +| GET | `/workspaces/users/{userId}/calendar/settings` | Get Workspace Calendar Free/Busy Event | `GetWorkspaceCalendarFree/BusyEvent` | +| POST | `/workspaces/users/{userId}/calendar/settings` | Set Workspace Calendar Free/Busy Event | `SetCalendarFree/BusyEvent` | +| GET | `/workspaces/users/{userId}/reservations` | Get a user's workspace's reservations | `userListReservations` | +| DELETE | `/workspaces/{locationId}/background` | Delete Workspace floor map | `DeleteWorkspaceFloorMap` | +| DELETE | `/workspaces/{workspaceId}` | Delete a workspace | `deleteWorkspace` | +| GET | `/workspaces/{workspaceId}` | Get a workspace | `getWorkspace` | +| PATCH | `/workspaces/{workspaceId}` | Update a workspace | `updateWorkspace` | +| DELETE | `/workspaces/{workspaceId}/assignment` | Delete a desk assignment | `Deleteadeskassignment` | +| GET | `/workspaces/{workspaceId}/assignment` | Get a desk assignment | `Getadeskassignment` | +| PUT | `/workspaces/{workspaceId}/assignment` | Set a desk assignment | `setADeskAssignment` | +| GET | `/workspaces/{workspaceId}/qr_code` | Get a workspace QR code | `getWorkspaceQRCode` | +| GET | `/workspaces/{workspaceId}/reservations` | Get a workspace's reservations | `listReservations` | +| POST | `/workspaces/{workspaceId}/reservations` | Create a reservation | `createReservation` | +| DELETE | `/workspaces/{workspaceId}/reservations/{reservationId}` | Delete a reservation | `deleteReservation` | +| GET | `/workspaces/{workspaceId}/reservations/{reservationId}` | Get a workspace reservation by reservationId | `GETGetaworkspacereservationbyreservationID` | +| PATCH | `/workspaces/{workspaceId}/reservations/{reservationId}` | Update a reservation | `updateReservation` | +| GET | `/workspaces/{workspaceId}/reservations/{reservationId}/questionnaires` | List workspace reservation questionnaires | `Listworkspacereservationquestionnaires` | + +### Zoom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms` | List Zoom Rooms | `listZoomRooms` | +| POST | `/rooms` | Add a Zoom Room | `addARoom` | +| GET | `/rooms/digital_signage` | List digital signage contents | `listDigitalSignageContent` | +| PATCH | `/rooms/events` | Update E911 digital signage | `manageE911signage` | +| PATCH | `/rooms/{id}/events` | Use Zoom Room controls | `ZoomRoomsControls` | +| GET | `/rooms/{id}/settings` | Get Zoom Room settings | `getZRSettings` | +| PATCH | `/rooms/{id}/settings` | Update Zoom Room settings | `updateZRSettings` | +| DELETE | `/rooms/{roomId}` | Delete a Zoom Room | `deleteAZoomRoom` | +| GET | `/rooms/{roomId}` | Get Zoom Room profile | `getZRProfile` | +| PATCH | `/rooms/{roomId}` | Update a Zoom Room profile | `updateRoomProfile` | +| GET | `/rooms/{roomId}/device_profiles` | List device profiles | `getRoomProfiles` | +| POST | `/rooms/{roomId}/device_profiles` | Create a device profile | `createRoomDeviceProfile` | +| GET | `/rooms/{roomId}/device_profiles/devices` | Get device information | `getRoomDevices` | +| DELETE | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Delete a device profile | `deleteRoomProfile` | +| GET | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Get a device profile | `getRoomProfile` | +| PATCH | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Update a device profile | `updateDeviceProfile` | +| GET | `/rooms/{roomId}/devices` | List Zoom Room devices | `listZRDevices` | +| PUT | `/rooms/{roomId}/location` | Change a Zoom Room's location | `changeZRLocation` | +| GET | `/rooms/{roomId}/sensor_data` | Get Zoom Room sensor data | `getZRSensorData` | +| GET | `/rooms/{roomId}/virtual_controller` | Get Zoom Rooms virtual controller URL | `getWebzrcUrl` | + +### Zoom Rooms Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/account_profile` | Get Zoom Room account profile | `getZRAccountProfile` | +| PATCH | `/rooms/account_profile` | Update Zoom Room account profile | `updateZRAccProfile` | +| GET | `/rooms/account_settings` | Get Zoom Room account settings | `getZRAccountSettings` | +| PATCH | `/rooms/account_settings` | Update Zoom Room account settings | `updateZoomRoomAccSettings` | + +### Zoom Rooms Calendar + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/calendar/services` | List calendar services | `getCalendarServices` | +| DELETE | `/rooms/calendar/services/{serviceId}` | Delete a calendar service | `deleteACalendarService` | +| GET | `/rooms/calendar/services/{serviceId}/resources` | List calendar resources by calendar service | `getCalendarResourcesByServiceId` | +| POST | `/rooms/calendar/services/{serviceId}/resources` | Add a calendar resource to a calendar service | `addACalendarResourceToCalendarService` | +| DELETE | `/rooms/calendar/services/{serviceId}/resources/{resourceId}` | Delete a calendar resource | `deleteACalendarResource` | +| GET | `/rooms/calendar/services/{serviceId}/resources/{resourceId}` | Get a calendar resource by ID | `getCalendarResourceById` | +| PUT | `/rooms/calendar/services/{serviceId}/sync` | Start calendar service sync process | `syncACalendarService` | + +### Zoom Rooms Content + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/content/background/contents` | List Zoom Rooms background image library contents | `ListZoomRoomsbackgroundimagelibrarycontents` | +| DELETE | `/rooms/content/background/contents/{contentId}` | Delete Zoom Rooms Background Image Library Content | `DeleteZoomRoomsBackgroundImageLibraryContent` | +| GET | `/rooms/content/background/contents/{contentId}` | Get Zoom Rooms background image library content | `GetZoomRoomsBackgroundImageLibraryContent` | +| GET | `/rooms/content/background/defaults` | List default Zoom Rooms background image library contents | `ListDefaultZoomRoomsBackgroundImageLibrarycontents` | +| GET | `/rooms/content/background/folders` | List Zoom Rooms Background Image Library Folders | `ListZoomRoomsBackgroundLibraryFolders` | +| POST | `/rooms/content/background/folders` | Add Zoom Rooms Background Image Library Folder | `AddZoomRoomsBackgroundLibraryFolder` | +| DELETE | `/rooms/content/background/folders/{folderId}` | Delete Zoom Rooms Background Image Library Folder | `DeleteZoomRoomsBackgroundLibraryFolder` | +| GET | `/rooms/content/background/folders/{folderId}` | Get Zoom Rooms Background Image Library Folder | `GetZoomRoomsBackgroundLibraryFolder` | +| PATCH | `/rooms/content/background/folders/{folderId}` | Update Zoom Rooms Background Image Library Folder | `UpdateaZoomRoomsBackgroundLibraryFolderName` | +| GET | `/rooms/content/digital_signage/contents` | List Digital Signage content items | `GETListdigitalsignagecontentitems` | +| POST | `/rooms/content/digital_signage/contents` | Add a digital signage URL | `AddadigitalsignageURL` | +| DELETE | `/rooms/content/digital_signage/contents/{contentId}` | Delete a Digital Signage content item | `Deleteadigitalsignagecontentitem` | +| GET | `/rooms/content/digital_signage/contents/{contentId}` | Get Digital Signage content item | `Getdigitalsignagecontentitem` | +| PATCH | `/rooms/content/digital_signage/contents/{contentId}` | Update a Digital Signage content item attributes | `Updateadigitalsignagecontentitemattributes` | +| POST | `/rooms/content/digital_signage/folders` | Add a digital signage content folder | `Addadigitalsignagecontentfolder` | +| DELETE | `/rooms/content/digital_signage/folders/{folderId}` | Delete a Digital Signage content folder | `Deleteadigitalsignagecontentfolder` | +| GET | `/rooms/content/digital_signage/folders/{folderId}` | Get Digital Signage content folder | `Getdigitalsignagecontentfolderdetails` | +| PATCH | `/rooms/content/digital_signage/folders/{folderId}` | Update a digital signage content folder | `Updateadigitalsignagecontentfolder` | +| GET | `/rooms/content/digital_signage/playlists` | List Digital Signage library playlists | `ListDigitalSignagelibraryplaylists` | +| POST | `/rooms/content/digital_signage/playlists` | Add a Digital Signage library playlist | `AddaDigitalSignagelibraryplaylist` | +| DELETE | `/rooms/content/digital_signage/playlists/{playlistId}` | Delete Digital Signage library playlist | `DeleteDigitalSignagelibraryplaylist` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}` | Get Digital Signage library playlist | `GetDigitalSignagelibraryplaylist` | +| PATCH | `/rooms/content/digital_signage/playlists/{playlistId}` | Update a Digital Signage library playlist | `UpdateaDigitalSignagelibraryplaylist` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}/contents` | Get Digital Signage library playlist content items | `GetDigitalSignagelibraryplaylistcontentitems` | +| PUT | `/rooms/content/digital_signage/playlists/{playlistId}/contents` | Update Digital Signage library playlist content items | `UpdateDigitalSignagelibraryplaylistcontentitems` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}/rooms` | List Digital Signage library playlist published rooms | `ListDigitalSignagelibraryplaylistpublishedrooms` | +| PUT | `/rooms/content/digital_signage/playlists/{playlistId}/rooms` | Update Digital Signage library playlist published rooms | `UpdateDigitalSignagelibraryplaylistpublishedrooms` | +| POST | `/zrbackground/files` | Add Zoom Rooms background image library content | `AddZoomRoomsBackgroundImageLibraryContent` | +| PUT | `/zrbackground/files` | Update Zoom Rooms background image library content | `UpdateZoomRoomsBackgroundImageLibraryContent` | +| POST | `/zrdigitalsignage/files` | Add a digital signage image or video | `Adddigitalsignageimageorvideo` | +| PUT | `/zrdigitalsignage/files` | Update a Digital Signage image or video file | `Updateadigitalsignageimageorvideofile` | + +### Zoom Rooms Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/rooms/{roomId}/devices/{deviceId}` | Delete a Zoom Room user device | `deleteDevice` | +| PUT | `/rooms/{roomId}/devices/{deviceId}/app_version` | Change Zoom Rooms app version | `changeZoomRoomsAppVersion` | + +### Zoom Rooms Location + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/locations` | List Zoom Room locations | `listZRLocations` | +| POST | `/rooms/locations` | Add a location | `addAZRLocation` | +| GET | `/rooms/locations/structure` | Get Zoom Room location structure | `getZRLocationStructure` | +| PUT | `/rooms/locations/structure` | Update Zoom Rooms location structure | `updateZoomRoomsLocationStructure` | +| DELETE | `/rooms/locations/{locationId}` | Delete a location | `deleteAZRLocation` | +| GET | `/rooms/locations/{locationId}` | Get Zoom Room location profile | `getZRLocationProfile` | +| PATCH | `/rooms/locations/{locationId}` | Update Zoom Room location profile | `updateZRLocationProfile` | +| PUT | `/rooms/locations/{locationId}/location` | Change the assigned parent location | `changeParentLocation` | +| GET | `/rooms/locations/{locationId}/settings` | Get location settings | `getZRLocationSettings` | +| PATCH | `/rooms/locations/{locationId}/settings` | Update location settings | `updateZRLocationSettings` | + +### Zoom Rooms Tags + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/rooms/locations/{locationId}/tags` | Assign Tags to Zoom Rooms By Location ID | `AssignTagsToZoomRoomsByLocationID` | +| GET | `/rooms/tags` | List all Zoom Room Tags | `listZoomRoomTags` | +| POST | `/rooms/tags` | Create a new Zoom Rooms Tag | `createZoomRoomTag` | +| DELETE | `/rooms/tags/{tagId}` | Delete Tag | `deleteZoomRoomTag` | +| PATCH | `/rooms/tags/{tagId}` | Edit Tag | `editZoomRoomTag` | +| DELETE | `/rooms/{roomId}/tags` | Un-assign Tags from a Zoom Room | `unassignZoomRoomTag` | +| PATCH | `/rooms/{roomId}/tags` | Assign Tags to a Zoom Room | `AssignTagsToAZoomRoom` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/scheduler.md b/partner-built/zoom-plugin/skills/rest-api/references/scheduler.md new file mode 100644 index 00000000..b7a165ef --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/scheduler.md @@ -0,0 +1,105 @@ +# Zoom Scheduler API + +Authoritative endpoint inventory for Scheduler. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/scheduler/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 21 | +| Path templates | 13 | +| Tags | 9 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| analytics | 1 | +| availability | 5 | +| Routing Forms | 1 | +| scheduled events | 5 | +| schedules | 5 | +| scheduling links | 1 | +| shares | 1 | +| team | 1 | +| users | 1 | + +## Endpoints by Tag + +### analytics + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/analytics` | Report analytics | `report_analytics` | + +### availability + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/availability` | List availability | `list_availability` | +| POST | `/scheduler/availability` | Insert availability | `insert_availability` | +| DELETE | `/scheduler/availability/{availabilityId}` | Delete availability | `delete_availability` | +| GET | `/scheduler/availability/{availabilityId}` | Get availability | `get_availability` | +| PATCH | `/scheduler/availability/{availabilityId}` | Patch availability | `patch_availability` | + +### Routing Forms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/routing/forms/{formId}/response/{responseId}` | get routing response | `Getroutingresponse` | + +### scheduled events + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/events` | List scheduled events | `list_scheduled_events` | +| DELETE | `/scheduler/events/{eventId}` | Delete scheduled events | `delete_scheduled_events` | +| GET | `/scheduler/events/{eventId}` | Get scheduled events | `get_scheduled_events` | +| PATCH | `/scheduler/events/{eventId}` | Patch scheduled events | `patch_scheduled_events` | +| GET | `/scheduler/events/{eventId}/attendees/{attendeeId}` | Get scheduled event attendee | `get_scheduled_event_attendee` | + +### schedules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/schedules` | List schedules | `list_schedules` | +| POST | `/scheduler/schedules` | Insert schedules | `insert_schedule` | +| DELETE | `/scheduler/schedules/{scheduleId}` | Delete schedules | `delete_schedules` | +| GET | `/scheduler/schedules/{scheduleId}` | Get schedules | `get_schedule` | +| PATCH | `/scheduler/schedules/{scheduleId}` | Patch schedules | `patch_schedule` | + +### scheduling links + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/scheduler/schedules/single_use_link` | Single use link | `single_use_link` | + +### shares + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/scheduler/shares` | Create shares | `create_shares` | + +### team + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/teams` | List team | `Listteam` | + +### users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/users/{userId}` | Get user | `get_user` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/scim2.md b/partner-built/zoom-plugin/skills/rest-api/references/scim2.md new file mode 100644 index 00000000..09fe3236 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/scim2.md @@ -0,0 +1,53 @@ +# Zoom SCIM2 API + +Authoritative endpoint inventory for SCIM2. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/scim2/methods/endpoints.json +- Base URL: `https://api.zoom.us` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 11 | +| Path templates | 4 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Group | 5 | +| User | 6 | + +## Endpoints by Tag + +### Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scim2/Groups` | List groups | `groupSCIM2List` | +| POST | `/scim2/Groups` | Create a group | `groupScim2Create` | +| DELETE | `/scim2/Groups/{groupId}` | Delete a group | `groupSCIM2Delete` | +| GET | `/scim2/Groups/{groupId}` | Get a group | `groupSCIM2Get` | +| PATCH | `/scim2/Groups/{groupId}` | Update a group | `groupSCIM2Update` | + +### User + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scim2/Users` | List users | `userSCIM2List` | +| POST | `/scim2/Users` | Create a user | `userScim2Create` | +| DELETE | `/scim2/Users/{userId}` | Delete a user | `userSCIM2Delete` | +| GET | `/scim2/Users/{userId}` | Get a user | `userSCIM2Get` | +| PATCH | `/scim2/Users/{userId}` | Deactivate a user | `userADSCIM2Deactivate` | +| PUT | `/scim2/Users/{userId}` | Update a user | `userSCIM2Update` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/tasks.md b/partner-built/zoom-plugin/skills/rest-api/references/tasks.md new file mode 100644 index 00000000..6f939448 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/tasks.md @@ -0,0 +1,68 @@ +# Zoom Tasks API + +Authoritative endpoint inventory for Tasks. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/tasks/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Assignee | 3 | +| Collaborator | 3 | +| Comment | 3 | +| Tasks | 5 | + +## Endpoints by Tag + +### Assignee + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/assignees` | Get assignees of a task | `GetAssigneesOfATask` | +| POST | `/tasks/items/{taskId}/assignees` | Add assignees to a task | `addTasksAssignees` | +| DELETE | `/tasks/items/{taskId}/assignees/{userId}` | Remove Assignee from task | `removeTaskAssignee` | + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/collaborators` | Get collaborators of a task | `Getcollaboratorsofatask` | +| POST | `/tasks/items/{taskId}/collaborators` | Add collaborators to a task | `addTasksCollaborators` | +| DELETE | `/tasks/items/{taskId}/collaborators/{userId}` | Remove collaborator from task | `removeTaskCollaborator` | + +### Comment + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/comments` | Get a task's comments | `GetAV1TasksComment` | +| POST | `/tasks/items/{taskId}/comments` | Add a comment to task | `addComment` | +| DELETE | `/tasks/items/{taskId}/comments/{commentId}` | Delete a task's comment | `DeleteTaskComment` | + +### Tasks + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items` | List tasks | `getMyTasks` | +| POST | `/tasks/items` | Create a new task | `createTask` | +| DELETE | `/tasks/items/{taskId}` | Delete a task | `deleteTask` | +| GET | `/tasks/items/{taskId}` | Get task details | `getTaskDetail` | +| PATCH | `/tasks/items/{taskId}` | Update task fields | `updateTask` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/team-chat.md b/partner-built/zoom-plugin/skills/rest-api/references/team-chat.md new file mode 100644 index 00000000..16bafa90 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/team-chat.md @@ -0,0 +1,233 @@ +# Zoom Team Chat API + +Authoritative endpoint inventory for Team Chat. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/team-chat/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 107 | +| Path templates | 69 | +| Tags | 16 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Chat Channel Mention Group | 7 | +| Chat Channels | 16 | +| Chat Channels (Account-level) | 16 | +| Chat Emoji | 3 | +| Chat Files | 4 | +| Chat Messages | 15 | +| Chat Migration | 7 | +| Chat Reminder | 3 | +| Chat Sessions | 2 | +| Contacts | 3 | +| IM Chat | 1 | +| IM Groups | 8 | +| Invitations | 1 | +| Legal Hold | 6 | +| Reports | 2 | +| Shared Spaces | 13 | + +## Endpoints by Tag + +### Chat Channel Mention Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/channels/{channelId}/mention_group` | List channel mention groups | `getChannelMentionGroup` | +| POST | `/chat/channels/{channelId}/mention_group` | Create a channel mention group | `createChannelMentionGroup` | +| DELETE | `/chat/channels/{channelId}/mention_group/{mentionGroupId}` | Delete a channel mention group | `deleteAChannelMentionGroup` | +| PATCH | `/chat/channels/{channelId}/mention_group/{mentionGroupId}` | Update a channel mention group information | `updateChannelMentionGroup` | +| DELETE | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | Remove channel mention group members | `removeChannelMentionGroupMembers` | +| GET | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | List the members of a mention group | `listTheMembersOfMentionGroup` | +| POST | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | Add channel members to a mention group | `addAChannelMembersToMentionGroup` | + +### Chat Channels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/activities/channels` | List channel activity logs | `listAllChannelActivityLogs` | +| PATCH | `/chat/channels/events` | Perform operations on channels | `PerformOperationsOnChannels` | +| DELETE | `/chat/channels/{channelId}` | Delete a channel | `deleteUserLevelChannel` | +| GET | `/chat/channels/{channelId}` | Get a channel | `getUserLevelChannel` | +| PATCH | `/chat/channels/{channelId}` | Update a channel | `updateUserLevelChannel` | +| DELETE | `/chat/channels/{channelId}/members` | Batch remove members from a channel | `batchRemoveChannelMembers` | +| GET | `/chat/channels/{channelId}/members` | List channel members | `listUserLevelChannelMembers` | +| POST | `/chat/channels/{channelId}/members` | Invite channel members | `InviteUserLevelChannelMembers` | +| GET | `/chat/channels/{channelId}/members/groups` | List channel members (Groups) | `listChannelMembersGroups` | +| POST | `/chat/channels/{channelId}/members/groups` | Invite channel members (Groups) | `inviteChannelMembersGroups` | +| DELETE | `/chat/channels/{channelId}/members/groups/{groupId}` | Remove a member (group) | `removeAMemberGroup` | +| DELETE | `/chat/channels/{channelId}/members/me` | Leave a channel | `leaveChannel` | +| POST | `/chat/channels/{channelId}/members/me` | Join a channel | `joinChannel` | +| DELETE | `/chat/channels/{channelId}/members/{identifier}` | Remove a member | `removeAUserLevelChannelMember` | +| GET | `/chat/users/{userId}/channels` | List user's channels | `getChannels` | +| POST | `/chat/users/{userId}/channels` | Create a channel | `createChannel` | + +### Chat Channels (Account-level) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/channels` | List account's public channels | `getAccountChannels` | +| POST | `/chat/channels/search` | Search user's or account's channels | `searchChannels` | +| GET | `/chat/channels/{channelId}/activities` | List channel activity logs | `listChannelActivityLogs` | +| GET | `/chat/channels/{channelId}/retention` | Get retention policy of a channel | `getChannelRetention` | +| PATCH | `/chat/channels/{channelId}/retention` | Update retention policy of a channel | `updateChannelRetention` | +| DELETE | `/chat/users/{userId}/channels` | Batch delete channels | `batchDeleteChannelsAccountLevel` | +| DELETE | `/chat/users/{userId}/channels/{channelId}` | Delete a channel | `deleteChannel` | +| GET | `/chat/users/{userId}/channels/{channelId}` | Get a channel | `getChannel` | +| PATCH | `/chat/users/{userId}/channels/{channelId}` | Update a channel | `updateChannel` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/admins` | Batch demote channel administrators | `batchDemoteChannelAdministrators` | +| GET | `/chat/users/{userId}/channels/{channelId}/admins` | List channel administrators | `listChannelAdministrators` | +| POST | `/chat/users/{userId}/channels/{channelId}/admins` | Promote channel members to administrators | `promoteChannelMembersAsAdmin` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/members` | Batch remove members from a user's channel | `batchRemoveUserChannelMembers` | +| GET | `/chat/users/{userId}/channels/{channelId}/members` | List channel members | `listChannelMembers` | +| POST | `/chat/users/{userId}/channels/{channelId}/members` | Invite channel members | `inviteChannelMembers` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/members/{identifier}` | Remove a member | `removeAChannelMember` | + +### Chat Emoji + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/emoji` | List custom emojis | `listCustomEmojis` | +| POST | `/chat/emoji/files` | Add a custom emoji | `addACustomEmoji` | +| DELETE | `/chat/emoji/{fileId}` | Delete a custom emoji | `DeleteCustomEmoji` | + +### Chat Files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/chat/files/{fileId}` | Delete a chat file | `deleteChatFile` | +| GET | `/chat/files/{fileId}` | Get file info | `getFileInfo` | +| POST | `/chat/users/{userId}/files` | Upload a chat file | `uploadAChatFile` | +| POST | `/chat/users/{userId}/messages/files` | Send a chat file | `sendChatFile` | + +### Chat Messages + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/chat/channel/message/events` | Perform operations on the message of channel | `PerformMessageOfChannel` | +| GET | `/chat/channels/{channelId}/pinned` | List pinned history messages of channel | `listChannelPinnedMessages` | +| GET | `/chat/forwarded_message/{forwardId}` | Get a forwarded message | `getForwardedMessage` | +| GET | `/chat/messages/bookmarks` | List bookmarks | `fetchBookmarks` | +| PATCH | `/chat/messages/bookmarks` | Add or remove a bookmark | `addOrRemoveABookmark` | +| GET | `/chat/messages/schedule` | List scheduled messages | `listScheduledMessages` | +| DELETE | `/chat/messages/schedule/{draftId}` | Delete a scheduled message | `deleteScheduleMessage` | +| GET | `/chat/users/{userId}/messages` | List user's chat messages | `getChatMessages` | +| POST | `/chat/users/{userId}/messages` | Send a chat message | `sendaChatMessage` | +| DELETE | `/chat/users/{userId}/messages/{messageId}` | Delete a message | `deleteChatMessage` | +| GET | `/chat/users/{userId}/messages/{messageId}` | Get a message | `getChatMessage` | +| PUT | `/chat/users/{userId}/messages/{messageId}` | Update a message | `editMessage` | +| PATCH | `/chat/users/{userId}/messages/{messageId}/emoji_reactions` | React to a chat message | `reactMessage` | +| PATCH | `/chat/users/{userId}/messages/{messageId}/status` | Mark message read or unread | `markMessage` | +| GET | `/chat/users/{userId}/messages/{messageId}/thread` | Retrieve a thread | `retrieveThread` | + +### Chat Migration + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/chat/migration/channels/{channelId}/members` | Migrate channel members | `MigrateChannelMembers` | +| POST | `/chat/migration/emoji_reactions` | Migrate chat message reactions | `MigrateChatMessageReactions` | +| GET | `/chat/migration/mappings/channels` | Get migrated Zoom channel IDs | `getMigrationChannelsMapping` | +| GET | `/chat/migration/mappings/users` | Get migrated Zoom user IDs | `getMigrationUsersMapping` | +| POST | `/chat/migration/messages` | Migrate chat messages | `MigrateChatMessages` | +| POST | `/chat/migration/users/{identifier}/channels` | Migrate a chat channel | `MigrateAChatChannel` | +| POST | `/chat/migration/users/{identifier}/events` | Migrate 1:1 conversation or channel operations | `Migrate1:1ConversationOrChannelOperations` | + +### Chat Reminder + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/chat/messages/{messageId}/reminder` | Delete a reminder for a message | `deleteReminderForMessage` | +| POST | `/chat/messages/{messageId}/reminder` | Create a reminder message | `createReminderForMessage` | +| GET | `/chat/reminder` | List reminders | `listReminders` | + +### Chat Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/chat/users/{userId}/events` | Star or unstar a channel or contact user | `starUnstarChannelContact` | +| GET | `/chat/users/{userId}/sessions` | List a user's chat sessions | `getChatSessions` | + +### Contacts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/users/me/contacts` | List user's contacts | `getUserContacts` | +| GET | `/chat/users/me/contacts/{identifier}` | Get user's contact details | `getUserContact` | +| GET | `/contacts` | Search company contacts | `searchCompanyContacts` | + +### IM Chat + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/im/users/me/chat/messages` | Send IM messages | `sendimmessages` | + +### IM Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/im/groups` | List IM directory groups | `imGroups` | +| POST | `/im/groups` | Create an IM directory group | `imGroupCreate` | +| DELETE | `/im/groups/{groupId}` | Delete an IM directory group | `imGroupDelete` | +| GET | `/im/groups/{groupId}` | Retrieve an IM directory group | `imGroup` | +| PATCH | `/im/groups/{groupId}` | Update an IM directory group | `imGroupUpdate` | +| GET | `/im/groups/{groupId}/members` | List IM directory group members | `imGroupMembers` | +| POST | `/im/groups/{groupId}/members` | Add IM directory group members | `imGroupMembersCreate` | +| DELETE | `/im/groups/{groupId}/members/{memberId}` | Delete IM directory group member | `imGroupMembersDelete` | + +### Invitations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/chat/users/{userId}/invitations` | Send new contact invitation | `sendNewContactInvitation` | + +### Legal Hold + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/legalhold/matters` | List legal hold matters | `listLegalHoldMatters` | +| POST | `/chat/legalhold/matters` | Add a legal hold matter | `addLegalHoldMatter` | +| DELETE | `/chat/legalhold/matters/{matterId}` | Delete legal hold matters | `deleteLegalHoldMatters` | +| PATCH | `/chat/legalhold/matters/{matterId}` | Update legal hold matter | `updateLegalHoldMatter` | +| GET | `/chat/legalhold/matters/{matterId}/files` | List legal hold files by given matter | `listLegalHoldFiles` | +| GET | `/chat/legalhold/matters/{matterId}/files/download` | Download legal hold files for given matter | `downloadLegalHoldFiles` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/report/chat/sessions` | Get chat sessions reports | `reportChatSessions` | +| GET | `/report/chat/sessions/{sessionId}` | Get chat message reports | `reportChatMessages` | + +### Shared Spaces + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/spaces` | List shared spaces | `listSharedSpaces` | +| POST | `/chat/spaces` | Create a shared space | `createSpace` | +| DELETE | `/chat/spaces/{spaceId}` | Delete a shared space | `deleteSpace` | +| GET | `/chat/spaces/{spaceId}` | Get a shared space | `getASharedSpace` | +| PATCH | `/chat/spaces/{spaceId}` | Update shared space settings | `updateSharedSpaceSettings` | +| DELETE | `/chat/spaces/{spaceId}/admins` | Demote shared space administrators to members | `demoteSpaceAdmins` | +| POST | `/chat/spaces/{spaceId}/admins` | Promote shared space members to administrators | `promoteSpaceMembers` | +| GET | `/chat/spaces/{spaceId}/channels` | List shared space channels | `listSharedSpaceChannels` | +| PATCH | `/chat/spaces/{spaceId}/channels` | Move shared space channels | `updateSharedSpaceChannels` | +| DELETE | `/chat/spaces/{spaceId}/members` | Remove members from a shared space | `deleteSpaceMembers` | +| GET | `/chat/spaces/{spaceId}/members` | List shared space members | `listSharedSpaceMembers` | +| POST | `/chat/spaces/{spaceId}/members` | Add members to a shared space | `addSpaceMembers` | +| PATCH | `/chat/spaces/{spaceId}/owner` | Transfer shared space ownership | `transferSpaceOwner` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/users.md b/partner-built/zoom-plugin/skills/rest-api/references/users.md new file mode 100644 index 00000000..3424b891 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/users.md @@ -0,0 +1,125 @@ +# Zoom Users API + +Authoritative endpoint inventory for Users. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/users/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 71 | +| Path templates | 41 | +| Tags | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Contact Groups | 8 | +| Divisions | 7 | +| Groups | 21 | +| Users | 35 | + +## Endpoints by Tag + +### Contact Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contacts/groups` | List contact groups | `contactGroups` | +| POST | `/contacts/groups` | Create a contact group | `contactGroupCreate` | +| DELETE | `/contacts/groups/{groupId}` | Delete a contact group | `contactGroupDelete` | +| GET | `/contacts/groups/{groupId}` | Get a contact group | `contactGroup` | +| PATCH | `/contacts/groups/{groupId}` | Update a contact group | `contactGroupUpdate` | +| DELETE | `/contacts/groups/{groupId}/members` | Remove members in a contact group | `contactGroupMemberRemove` | +| GET | `/contacts/groups/{groupId}/members` | List contact group members | `contactGroupMembers` | +| POST | `/contacts/groups/{groupId}/members` | Add contact group members | `contactGroupMemberAdd` | + +### Divisions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/divisions` | List divisions | `listDivisions` | +| POST | `/divisions` | Create a division | `Createadivision` | +| DELETE | `/divisions/{divisionId}` | Delete a division | `Deletedivision` | +| GET | `/divisions/{divisionId}` | Get a division | `Getdivision` | +| PATCH | `/divisions/{divisionId}` | Update a division | `Updateadivision` | +| GET | `/divisions/{divisionId}/users` | List division members | `listDivisionMembers` | +| POST | `/divisions/{divisionId}/users` | Assign a division | `assigndivisionMember` | + +### Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/groups` | List groups | `groups` | +| POST | `/groups` | Create a group | `groupCreate` | +| DELETE | `/groups/{groupId}` | Delete a group | `groupDelete` | +| GET | `/groups/{groupId}` | Get a group | `group` | +| PATCH | `/groups/{groupId}` | Update a group | `groupUpdate` | +| GET | `/groups/{groupId}/admins` | List group admins | `groupAdmins` | +| POST | `/groups/{groupId}/admins` | Add group admins | `groupAdminsCreate` | +| DELETE | `/groups/{groupId}/admins/{userId}` | Delete a group admin | `groupAdminsDelete` | +| GET | `/groups/{groupId}/channels` | List group channels | `groupChannels` | +| GET | `/groups/{groupId}/lock_settings` | Get locked settings | `getGroupLockSettings` | +| PATCH | `/groups/{groupId}/lock_settings` | Update locked settings | `groupLockedSettings` | +| GET | `/groups/{groupId}/members` | List group members | `groupMembers` | +| POST | `/groups/{groupId}/members` | Add group members | `groupMembersCreate` | +| DELETE | `/groups/{groupId}/members/{memberId}` | Delete a group member | `groupMembersDelete` | +| PATCH | `/groups/{groupId}/members/{memberId}` | Update a group member | `updateAGroupMember` | +| GET | `/groups/{groupId}/settings` | Get a group's settings | `getGroupSettings` | +| PATCH | `/groups/{groupId}/settings` | Update a group's settings | `updateGroupSettings` | +| GET | `/groups/{groupId}/settings/registration` | Get a group's webinar registration settings | `groupSettingsRegistration` | +| PATCH | `/groups/{groupId}/settings/registration` | Update a group's webinar registration settings | `groupSettingsRegistrationUpdate` | +| DELETE | `/groups/{groupId}/settings/virtual_backgrounds` | Delete Virtual Background files | `delGroupVB` | +| POST | `/groups/{groupId}/settings/virtual_backgrounds` | Upload Virtual Background files | `uploadGroupVB` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users` | List users | `users` | +| POST | `/users` | Create users | `userCreate` | +| GET | `/users/email` | Check a user email | `userEmail` | +| POST | `/users/features` | Bulk update features for users | `bulkUpdateFeature` | +| GET | `/users/me/zak` | Get the user's ZAK | `userZak` | +| GET | `/users/summary` | Get user summary | `userSummary` | +| GET | `/users/vanity_name` | Check a user's PM room | `userVanityName` | +| DELETE | `/users/{userId}` | Delete a user | `userDelete` | +| GET | `/users/{userId}` | Get a user | `user` | +| PATCH | `/users/{userId}` | Update a user | `userUpdate` | +| DELETE | `/users/{userId}/assistants` | Delete user assistants | `userAssistantsDelete` | +| GET | `/users/{userId}/assistants` | List user assistants | `userAssistants` | +| POST | `/users/{userId}/assistants` | Add assistants | `userAssistantCreate` | +| DELETE | `/users/{userId}/assistants/{assistantId}` | Delete a user assistant | `userAssistantDelete` | +| GET | `/users/{userId}/collaboration_devices` | List a user's collaboration devices | `listCollaborationDevices` | +| GET | `/users/{userId}/collaboration_devices/{collaborationDeviceId}` | Get collaboration device detail | `getCollaborationDevice` | +| PUT | `/users/{userId}/email` | Update a user's email | `userEmailUpdate` | +| GET | `/users/{userId}/meeting_summary_templates` | Get meeting summary templates | `Getmeetingsummarytemplates` | +| GET | `/users/{userId}/meeting_templates/{meetingTemplateId}` | Get meeting template detail | `getUserMeetingTemplates` | +| PUT | `/users/{userId}/password` | Update a user's password | `userPassword` | +| GET | `/users/{userId}/permissions` | Get user permissions | `userPermission` | +| DELETE | `/users/{userId}/picture` | Delete a user's profile picture | `userPictureDelete` | +| POST | `/users/{userId}/picture` | Upload a user's profile picture | `userPicture` | +| GET | `/users/{userId}/presence_status` | Get a user presence status | `getUserPresenceStatus` | +| PUT | `/users/{userId}/presence_status` | Update a user's presence status | `updatePresenceStatus` | +| DELETE | `/users/{userId}/schedulers` | Delete user schedulers | `userSchedulersDelete` | +| GET | `/users/{userId}/schedulers` | List user schedulers | `userSchedulers` | +| DELETE | `/users/{userId}/schedulers/{schedulerId}` | Delete a scheduler | `userSchedulerDelete` | +| GET | `/users/{userId}/settings` | Get user settings | `userSettings` | +| PATCH | `/users/{userId}/settings` | Update user settings | `userSettingsUpdate` | +| DELETE | `/users/{userId}/settings/virtual_backgrounds` | Delete Virtual Background files | `delUserVB` | +| POST | `/users/{userId}/settings/virtual_backgrounds` | Upload Virtual Background files | `uploadVBuser` | +| PUT | `/users/{userId}/status` | Update user status | `userStatus` | +| DELETE | `/users/{userId}/token` | Revoke a user's SSO token | `userSSOTokenDelete` | +| GET | `/users/{userId}/token` | Get a user's token | `userToken` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/video-management.md b/partner-built/zoom-plugin/skills/rest-api/references/video-management.md new file mode 100644 index 00000000..64affe94 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/video-management.md @@ -0,0 +1,85 @@ +# Zoom Video Management API + +Authoritative endpoint inventory for Video Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/video-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 25 | +| Path templates | 11 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Channels | 6 | +| files | 1 | +| Permissions | 4 | +| Playlists | 7 | +| Videos | 7 | + +## Endpoints by Tag + +### Channels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/video_management/channels` | List channels | `listVideoChannels` | +| POST | `/video_management/channels` | Create a channel | `createChannel` | +| DELETE | `/video_management/channels/{channelId}` | Delete channel | `DeleteChannel` | +| GET | `/video_management/channels/{channelId}` | Get channel details | `getChannelDetail` | +| PATCH | `/video_management/channels/{channelId}` | Update channel | `UpdateVideoChannel` | +| PATCH | `/video_management/channels/{channelId}/actions` | Channel actions | `channelActions` | + +### files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/video_management/files` | Upload file for video management | `uploadVODtFile` | + +### Permissions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/permissions` | Delete channel permissions | `DeleteChannelPermissions` | +| GET | `/video_management/channels/{channelId}/permissions` | List channel permissions | `listChannelPermissions` | +| PATCH | `/video_management/channels/{channelId}/permissions` | Update channel permissions | `updateChannelPermissions` | +| POST | `/video_management/channels/{channelId}/permissions` | Create channel permissions | `createChannelPermissions` | + +### Playlists + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/playlists` | Delete channel playlists | `DeleteChannelPlaylists` | +| GET | `/video_management/channels/{channelId}/playlists` | List channel playlists | `ListChannelPlaylists` | +| POST | `/video_management/channels/{channelId}/playlists` | Add channel playlists | `AddChannelPlaylists` | +| GET | `/video_management/playlists` | List playlists | `ListPlaylists` | +| POST | `/video_management/playlists` | Create a playlist | `createPlaylist` | +| DELETE | `/video_management/playlists/{playlistId}` | Delete playlist | `DeletePlaylist` | +| PATCH | `/video_management/playlists/{playlistId}` | Update playlist | `UpdatePlaylist` | + +### Videos + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/videos` | Delete channel videos | `DeleteChannelVideos` | +| GET | `/video_management/channels/{channelId}/videos` | List channel videos | `ListChannelVideos` | +| POST | `/video_management/channels/{channelId}/videos` | Add channel videos | `AddChannelVideos` | +| DELETE | `/video_management/playlists/{playlistId}/videos` | Delete playlist videos | `DeletePlaylistVideos` | +| GET | `/video_management/playlists/{playlistId}/videos` | List playlist videos | `ListPlaylistVideos` | +| POST | `/video_management/playlists/{playlistId}/videos` | Add playlist videos | `AddPlaylistVideos` | +| GET | `/video_management/videos` | List all videos | `ListAllVideos` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/video-sdk-api.md b/partner-built/zoom-plugin/skills/rest-api/references/video-sdk-api.md new file mode 100644 index 00000000..5484f974 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/video-sdk-api.md @@ -0,0 +1,92 @@ +# Zoom Video SDK API + +Authoritative endpoint inventory for Video 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/video-sdk/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 38 | +| Path templates | 28 | +| Tags | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Byos Storage | 6 | +| Cloud Recording | 6 | +| Sessions | 21 | +| Video SDK Reports | 5 | + +## Endpoints by Tag + +### Byos Storage + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/videosdk/settings/storage` | Update Bring Your Own Storage settings | `byosStorageSwitchPatch` | +| GET | `/videosdk/settings/storage/location` | List storage location | `byosStorageGet` | +| POST | `/videosdk/settings/storage/location` | Add storage location | `byosStoragePost` | +| DELETE | `/videosdk/settings/storage/location/{storageLocationId}` | Delete storage location detail | `byosStorageDetailDelete` | +| GET | `/videosdk/settings/storage/location/{storageLocationId}` | Storage location detail | `byosStorageDetailGet` | +| PATCH | `/videosdk/settings/storage/location/{storageLocationId}` | Change storage location detail | `byosStorageDetailPatch` | + +### Cloud Recording + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/recordings` | List recordings of an account | `recordingsList` | +| DELETE | `/videosdk/sessions/{sessionId}/recordings` | Delete session's recordings | `recordingDelete` | +| GET | `/videosdk/sessions/{sessionId}/recordings` | List session's recordings | `recordingGet` | +| PUT | `/videosdk/sessions/{sessionId}/recordings/status` | Recover session's recordings | `recordingStatusUpdate` | +| DELETE | `/videosdk/sessions/{sessionId}/recordings/{recordingId}` | Delete session's recording file | `recordingDeleteOne` | +| PUT | `/videosdk/sessions/{sessionId}/recordings/{recordingId}/status` | Recover a single recording | `recordingStatusUpdateOne` | + +### Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/sessions` | List sessions | `sessions` | +| POST | `/videosdk/sessions` | Create a session | `Createasession` | +| DELETE | `/videosdk/sessions/{sessionId}` | Delete a session | `sessionDelete` | +| GET | `/videosdk/sessions/{sessionId}` | Get session details | `sessionDetail` | +| PATCH | `/videosdk/sessions/{sessionId}/events` | Use in-session events controls | `inSessionEventsControl` | +| GET | `/videosdk/sessions/{sessionId}/livestream` | Get session live stream details | `getLiveStreamDetails` | +| PATCH | `/videosdk/sessions/{sessionId}/livestream` | Update a session live stream | `sessionLiveStreamUpdate` | +| PATCH | `/videosdk/sessions/{sessionId}/livestream/status` | Update session livestream status | `sessionLiveStreamStatusUpdate` | +| PATCH | `/videosdk/sessions/{sessionId}/rtms_app/status` | Update Realtime Media Streams (RTMS) app status | `updateSessionRtmsAppStatus` | +| POST | `/videosdk/sessions/{sessionId}/sip_dialing` | Get a session SIP URI with passcode | `GetASessionSIPURIWithPasscode` | +| PUT | `/videosdk/sessions/{sessionId}/status` | Update session status | `sessionStatus` | +| GET | `/videosdk/sessions/{sessionId}/stream_ingestions` | List session streaming ingestions | `Listsessionstreamingingestions` | +| GET | `/videosdk/sessions/{sessionId}/users` | List session users | `sessionUsers` | +| GET | `/videosdk/sessions/{sessionId}/users/qos` | List session users QoS | `sessionUsersQOS` | +| GET | `/videosdk/sessions/{sessionId}/users/sharing` | Get sharing/recording details | `sessionParticipantShare` | +| GET | `/videosdk/sessions/{sessionId}/users/{userId}/qos` | Get session user QoS | `sessionUserQOS` | +| GET | `/videosdk/stream_ingestions` | List stream ingestions | `Liststreamingestions` | +| POST | `/videosdk/stream_ingestions` | Create a stream ingestion | `Createastreamingestion` | +| DELETE | `/videosdk/stream_ingestions/{streamId}` | Delete a stream ingestion | `Deleteastreamingestion` | +| GET | `/videosdk/stream_ingestions/{streamId}` | Get a stream ingestion | `Getastreamingestion` | +| PATCH | `/videosdk/stream_ingestions/{streamId}` | Update a stream ingestion | `Updateastreamingestion` | + +### Video SDK Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/report/cloud_recording` | Get cloud recording usage report | `vsdkReportCloudRecording` | +| GET | `/videosdk/report/daily` | Get daily usage report | `vsdkReportDaily` | +| GET | `/videosdk/report/operationlogs` | Get operation logs report | `vsdkReportOperationLogs` | +| GET | `/videosdk/report/telephone` | Get telephone report | `vsdkReportTelephone` | +| GET | `/videosdk/report/webhook_logs` | Get webhook logs | `getWebhookLogs` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/virtual-agent.md b/partner-built/zoom-plugin/skills/rest-api/references/virtual-agent.md new file mode 100644 index 00000000..ca42126e --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/virtual-agent.md @@ -0,0 +1,54 @@ +# Zoom Virtual Agent API + +Authoritative endpoint inventory for Virtual Agent. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/virtual-agent/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 12 | +| Path templates | 9 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Knowledge Management | 7 | +| Report | 5 | + +## Endpoints by Tag + +### Knowledge Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/km/kbs/{kbId}/articles` | Get articles | `GetArticles` | +| POST | `/km/kbs/{kbId}/articles` | Create article | `CreateArticle` | +| DELETE | `/km/kbs/{kbId}/articles/{articleId}` | Delete article | `DeleteArticle` | +| GET | `/km/kbs/{kbId}/articles/{articleId}` | Get article | `GetArticle` | +| PUT | `/km/kbs/{kbId}/articles/{articleId}` | Update article | `UpdateArticle` | +| POST | `/km/kbs/{kbId}/sync` | Create sync request | `CreateSyncRequest` | +| GET | `/km/kbs/{kbId}/sync/{syncId}` | Get sync | `GetSync` | + +### Report + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/virtual_agent/report/engagements` | Get ZVA engagements | `GetZVAEngagements` | +| GET | `/virtual_agent/report/engagements/query_details` | Get ZVA query details | `GetZVAQueryDetails` | +| GET | `/virtual_agent/report/engagements/variables` | Get ZVA variable details | `GetZVAengagementvariabledetails` | +| GET | `/virtual_agent/report/surveys` | Get ZVA Surveys | `GetZVASurveys` | +| GET | `/virtual_agent/report/transcripts` | Get ZVA transcripts | `GetZVATranscripts` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/webinars.md b/partner-built/zoom-plugin/skills/rest-api/references/webinars.md new file mode 100644 index 00000000..e1dd1240 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/webinars.md @@ -0,0 +1,76 @@ +# REST API - Webinars + +Webinar management endpoints. + +## Overview + +Create and manage Zoom webinars programmatically. + +## Endpoints + +### Create Webinar + +```bash +POST /users/{userId}/webinars +``` + +```json +{ + "topic": "My Webinar", + "type": 5, + "start_time": "2024-01-15T10:00:00Z", + "duration": 60, + "settings": { + "host_video": true, + "panelists_video": true, + "registration_type": 1 + } +} +``` + +### Get Webinar + +```bash +GET /webinars/{webinarId} +``` + +### Update Webinar + +```bash +PATCH /webinars/{webinarId} +``` + +### Delete Webinar + +```bash +DELETE /webinars/{webinarId} +``` + +### List Webinar Registrants + +```bash +GET /webinars/{webinarId}/registrants +``` + +### Add Registrant + +```bash +POST /webinars/{webinarId}/registrants +``` + +## Webinar Types + +| Type | Value | Description | +|------|-------|-------------| +| Webinar | 5 | Single webinar | +| Recurring (no fixed time) | 6 | Recurring, no schedule | +| Recurring (fixed time) | 9 | Recurring with schedule | + +## Required Scopes + +- `webinar:read` - View webinars +- `webinar:write` - Create/update/delete webinars + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Webinars diff --git a/partner-built/zoom-plugin/skills/rest-api/references/whiteboard.md b/partner-built/zoom-plugin/skills/rest-api/references/whiteboard.md new file mode 100644 index 00000000..671b456c --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/whiteboard.md @@ -0,0 +1,110 @@ +# Zoom Whiteboard API + +Authoritative endpoint inventory for Whiteboard. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/whiteboard/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 32 | +| Path templates | 21 | +| Tags | 8 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Archiving | 3 | +| Collaborator | 4 | +| Document | 5 | +| Export | 3 | +| File | 2 | +| Import | 2 | +| Project | 12 | +| Settings | 1 | + +## Endpoints by Tag + +### Archiving + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/sessions` | List whiteboards sessions | `Createwhiteboardsarchivefiles` | +| GET | `/whiteboards/sessions/activity/download/{path}` | Download Whiteboards activity file | `Downloadwhiteboardsactivityfile` | +| GET | `/whiteboards/sessions/{seesionId}` | List whiteboard sessions activities | `Listwhiteboardsessionsarchivedfiles` | + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/{whiteboardId}/collaborator` | Get collaborators of a whiteboard | `GetAWhiteboardCollaborator` | +| PATCH | `/whiteboards/{whiteboardId}/collaborator` | Update whiteboard collaborators | `UpdateAWhiteboardCollaborator` | +| POST | `/whiteboards/{whiteboardId}/collaborator` | Share a whiteboard to new users or team chat channels. | `AddAWhiteboardCollaborator` | +| DELETE | `/whiteboards/{whiteboardId}/collaborator/{collaboratorId}` | Remove the collaborator from a whiteboard | `DeleteAWhiteboardCollaborator` | + +### Document + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards` | List all whiteboards | `ListWhiteboards` | +| POST | `/whiteboards` | Create a new whiteboard | `newWhiteboardCreate` | +| DELETE | `/whiteboards/{whiteboardId}` | Delete a whiteboard | `DeleteAWhiteboard` | +| GET | `/whiteboards/{whiteboardId}` | Get a whiteboard | `GetAWhiteboard` | +| PUT | `/whiteboards/{whiteboardId}` | Update whiteboard basic information | `UpdateAWhiteboardMetadata` | + +### Export + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/export` | Create whiteboard export | `Createwhiteboardsexport` | +| GET | `/whiteboards/export/task/{taskId}` | Download whiteboard export | `Downloadwhiteboardexport` | +| GET | `/whiteboards/export/task/{taskId}/status` | Get whiteboard export generation status | `Getwhiteboardexportdatagenerationstatus` | + +### File + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/files` | Upload file for whiteboard import | `Uploadfileforwhiteboardimport` | +| GET | `/whiteboards/{whiteboardId}/files/{fileId}` | Download Imported Whiteboard File | `Downloadembeddedwhiteboardfile` | + +### Import + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/import` | Create a new whiteboard by import | `CreateWhiteboardImport` | +| GET | `/whiteboards/import/{taskId}/status` | Get whiteboard import status | `GetWhiteboardimportstatus` | + +### Project + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/projects` | List all projects | `Listallprojects` | +| POST | `/whiteboards/projects` | Create a new project | `Createproject` | +| DELETE | `/whiteboards/projects/{projectId}` | Delete a project | `Deleteproject` | +| GET | `/whiteboards/projects/{projectId}` | Get a project | `Getaproject` | +| PATCH | `/whiteboards/projects/{projectId}` | Update project basic information | `Updateproject` | +| GET | `/whiteboards/projects/{projectId}/collaborators` | Get collaborators of a project | `Getcollaboratorsofaproject` | +| PATCH | `/whiteboards/projects/{projectId}/collaborators` | Update project collaborators | `Updateprojectcollaborators` | +| POST | `/whiteboards/projects/{projectId}/collaborators` | Share a project to new users | `Shareaprojecttonewusers` | +| DELETE | `/whiteboards/projects/{projectId}/collaborators/{collaboratorId}` | Remove the collaborator from a project | `Removethecollaboratorfromaproject` | +| GET | `/whiteboards/projects/{projectId}/subprojects` | List subprojects | `listSubProjects` | +| DELETE | `/whiteboards/projects/{projectId}/whiteboards` | Remove whiteboards from a project | `Removewhiteboardsfromaproject` | +| POST | `/whiteboards/projects/{projectId}/whiteboards` | Move whiteboards to a project | `Movewhiteboardstoproject` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/whiteboards/{whiteboardId}/share_setting` | Update whiteboard share setting | `UpdateAWhiteboardShareSetting` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/workforce-management.md b/partner-built/zoom-plugin/skills/rest-api/references/workforce-management.md new file mode 100644 index 00000000..50c74a63 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/workforce-management.md @@ -0,0 +1,72 @@ +# Zoom Workforce Management API + +Authoritative endpoint inventory for Workforce Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/workforce-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 12 | +| Path templates | 9 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Forecasts | 2 | +| Imports | 2 | +| Organizational Groups | 5 | +| Reports | 2 | +| Schedules | 1 | + +## Endpoints by Tag + +### Forecasts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/forecasts` | List forecasts | `Listforecasts` | +| GET | `/workforce-management/forecasts/{forecastId}/scheduling-groups/{schedulingGroupId}` | Get a forecast for a scheduling group | `Getforecastofschedulinggroup` | + +### Imports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/workforce-management/imports/historical-queue-metrics` | Upload historical queue metrics | `Uploadhistoricalqueuemetrics` | +| GET | `/workforce-management/imports/{importId}/historical-queue-metrics` | Get historical queue metrics import metadata | `Viewhistoricalqueuemetricimoprtmeta` | + +### Organizational Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/organizational-groups` | Get multiple organizational groups | `getMultipleOrganizationalGroups` | +| POST | `/workforce-management/organizational-groups` | Create an organizational group | `createOrganizationalGroup` | +| DELETE | `/workforce-management/organizational-groups/{organizationalGroupId}` | Delete organizational group | `deleteOrganizationalGroup` | +| GET | `/workforce-management/organizational-groups/{organizationalGroupId}` | Get a single organizational group | `getOrganizationalGroup` | +| PATCH | `/workforce-management/organizational-groups/{organizationalGroupId}` | Update an organizational group | `updateOrganizationalGroup` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/reports/adherence/agents` | List agents' adherence data | `listAdherenceAgents` | +| GET | `/workforce-management/reports/schedules/agents` | List all schedule agents | `listScheduleAgents` | + +### Schedules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/schedules/agents` | List all schedule agents | `listSchedules` | diff --git a/partner-built/zoom-plugin/skills/rest-api/references/zoom-docs.md b/partner-built/zoom-plugin/skills/rest-api/references/zoom-docs.md new file mode 100644 index 00000000..92c80d55 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/references/zoom-docs.md @@ -0,0 +1,82 @@ +# Zoom Docs API + +Authoritative endpoint inventory for Zoom Docs. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/zoom-docs/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- 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 | 16 | +| Path templates | 11 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Collaborator | 4 | +| Export | 2 | +| File Management | 5 | +| File Uploads | 1 | +| General Access | 2 | +| Import | 2 | + +## Endpoints by Tag + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/docs/files/{fileId}/collaborators` | List collaborators of a file | `ListCollaborators` | +| POST | `/docs/files/{fileId}/collaborators` | Add collaborators for a file | `AddCollaborators` | +| DELETE | `/docs/files/{fileId}/collaborators/{collaboratorId}` | Remove a collaborator from a file | `RemoveACollaborator` | +| PATCH | `/docs/files/{fileId}/collaborators/{collaboratorId}` | Modify a collaborator’s role on a file | `ModifyCollaboratorRole` | + +### Export + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/exports` | Create a file export | `Createafileexport` | +| GET | `/docs/exports/{exportId}/status` | Get file export status | `Getfileexportstatus` | + +### File Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/files` | Create a new file | `CreateDoc` | +| DELETE | `/docs/files/{fileId}` | Delete a file | `DeleteFile` | +| GET | `/docs/files/{fileId}` | Get metadata of a file | `QueryFileMetadata` | +| PATCH | `/docs/files/{fileId}` | Modify metadata of a file | `ModifyMetadata` | +| GET | `/docs/files/{fileId}/children` | List all children of a file | `ListAllChildren` | + +### File Uploads + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/file_uploads` | Create file upload for docs import or attachments | `Uploadfilefordocsimportorattachments` | + +### General Access + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/docs/files/{fileId}/general_access_setting` | Get the general access setting of a file | `GetFileGeneralAccess` | +| PATCH | `/docs/files/{fileId}/general_access_setting` | Modify the general access setting of a file | `ModifyFileGeneralAccess` | + +### Import + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/imports` | Create a new file by import | `Createanewfilebyimport` | +| GET | `/docs/imports/{importId}/status` | Get file import status | `Getdocsfileimportstatus` | diff --git a/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-errors.md b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-errors.md new file mode 100644 index 00000000..9a84728f --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-errors.md @@ -0,0 +1,429 @@ +# Common Errors - HTTP Status Codes and Zoom Error Codes + +Complete reference for Zoom REST API error codes, HTTP status codes, and solutions. + +## HTTP Status Codes + +### 2XX - Success + +| Status | Description | Action | +|--------|-------------|--------| +| `200 OK` | Request succeeded | Parse response body | +| `201 Created` | Resource created | Check `Location` header for new resource URL | +| `204 No Content` | Request succeeded, no body | Common for UPDATE/DELETE operations | + +### 4XX - Client Errors + +| Status | Description | Common Causes | Solution | +|--------|-------------|---------------|----------| +| `400 Bad Request` | Invalid request | Missing required fields, invalid JSON, validation errors | Check request body format and required fields | +| `401 Unauthorized` | Authentication failed | Invalid/expired token, missing `Authorization` header | Refresh access token, check token format | +| `403 Forbidden` | Permission denied | Missing scopes, user lacks permission, shared access not granted | Add required scopes, check user role | +| `404 Not Found` | Resource doesn't exist | Invalid userId/meetingId, wrong `me` keyword usage | Verify resource ID, check `me` keyword rules | +| `409 Conflict` | Resource conflict | Email already exists, duplicate operation | Use unique identifiers | +| `429 Too Many Requests` | Rate limit exceeded | Too many requests per second/day | Implement exponential backoff, throttle requests | + +### 5XX - Server Errors + +| Status | Description | Action | +|--------|-------------|--------| +| `500 Internal Server Error` | Zoom server error | Retry with exponential backoff | +| `502 Bad Gateway` | Gateway error | Retry after delay | +| `503 Service Unavailable` | Zoom service down | Check [status.zoom.us](https://status.zoom.us), retry later | + +## Zoom Error Codes + +When an API call fails, Zoom returns an error response: + +```json +{ + "code": 300, + "message": "Request Body should be a valid JSON object." +} +``` + +### Common Zoom Error Codes + +| Code | HTTP | Message | Cause | Solution | +|------|------|---------|-------|----------| +| `300` | 400 | Invalid request | Bad JSON, validation error | Check request body structure | +| `124` | 400 | Invalid parameter | Wrong parameter type/value | Validate parameters against API docs | +| `200` | 401 | Invalid credentials | Incorrect OAuth token | Refresh access token | +| `201` | 401 | Access token expired | Token expired | Request new token | +| `1001` | 404 | User does not exist | Invalid userId or wrong `me` usage | Check userId, review `me` keyword rules | +| `300` | 404 | Meeting not found | Invalid meetingId | Verify meeting exists | +| `3000` | 404 | Cannot access webinar info | Webinar doesn't exist or no access | Check webinarId and scopes | +| `200` | 429 | Rate limit exceeded | Too many requests | Implement rate limiting | +| `4700` | 401 | Invalid access token | Token missing scopes | Add required scopes in app config | +| `3001` | 403 | Not allowed to access | Missing permission | Upgrade user role or add scope | + +## Detailed Error Scenarios + +### 401 Unauthorized + +#### Scenario 1: Token Expired + +```json +{ + "code": 201, + "message": "Access token is expired." +} +``` + +**Solution:** +```javascript +async function apiCallWithRetry(url, options) { + try { + const response = await fetch(url, options); + + if (response.status === 401) { + const error = await response.json(); + + if (error.code === 201) { + // Token expired - refresh and retry + const newToken = await refreshAccessToken(); + options.headers['Authorization'] = `Bearer ${newToken}`; + return await fetch(url, options); + } + } + + return response; + } catch (error) { + console.error('API call failed:', error); + throw error; + } +} +``` + +#### Scenario 2: Missing/Invalid Token + +```json +{ + "code": 200, + "message": "Invalid credentials." +} +``` + +**Solution:** +- Check `Authorization` header is present: `Authorization: Bearer ACCESS_TOKEN` +- Verify token format (should be JWT) +- Ensure token is for the correct account + +#### Scenario 3: Missing Scopes + +```json +{ + "code": 4700, + "message": "Invalid access token, does not contain scopes." +} +``` + +**Solution:** +1. Go to your app on Zoom Marketplace +2. Add required scopes (e.g., `meeting:write:admin`) +3. Request new access token with updated scopes + +### 403 Forbidden + +#### Scenario 1: Missing Permission + +```json +{ + "code": 3001, + "message": "This user is not allowed to access this resource." +} +``` + +**Solution:** +- Verify user has appropriate role (Admin, Owner) +- Check if endpoint requires admin-level scopes +- Use account-level scope (e.g., `meeting:write:admin` instead of `meeting:write`) + +#### Scenario 2: Shared Access Permissions + +```json +{ + "code": 3001, + "message": "Authenticated user has not permitted access to the targeted resource." +} +``` + +**Cause:** User hasn't authorized shared access permissions for your app. + +**Solution:** Direct user to: [Allowing Apps Access to Shared Access Permissions](https://support.zoom.us/hc/en-us/articles/4413265586189) + +### 404 Not Found + +#### Scenario 1: User Not Found (Wrong `me` Usage) + +```json +{ + "code": 1001, + "message": "User does not exist: user@example.com" +} +``` + +**Cause:** Using `userId` with User OAuth app (should use `me`). + +**Solution:** +```javascript +// WRONG (User OAuth app) +fetch('https://api.zoom.us/v2/users/user@example.com', { + headers: { 'Authorization': `Bearer ${userToken}` } +}); + +// CORRECT (User OAuth app) +fetch('https://api.zoom.us/v2/users/me', { + headers: { 'Authorization': `Bearer ${userToken}` } +}); +``` + +#### Scenario 2: Meeting/Resource Not Found + +```json +{ + "code": 300, + "message": "Meeting not found." +} +``` + +**Causes:** +- Meeting deleted +- Meeting expired (30 days after last use) +- Invalid meetingId +- Wrong UUID encoding + +**Solution for UUID:** +```javascript +// UUID starts with / or contains // → double-encode +const uuid = '/xyzAbC123=='; +const encoded = encodeURIComponent(encodeURIComponent(uuid)); +fetch(`https://api.zoom.us/v2/meetings/${encoded}`, { headers }); +``` + +### 429 Too Many Requests + +```json +{ + "code": 200, + "message": "You have reached the maximum per-second rate limit for this API." +} +``` + +**Solution:** Implement exponential backoff + +```javascript +async function apiCallWithBackoff(url, options, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const response = await fetch(url, options); + + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt); + console.log(`Rate limited. Retrying after ${retryAfter}s`); + await sleep(retryAfter * 1000); + continue; + } + + return response; + } + + throw new Error('Max retries exceeded'); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +``` + +**Check rate limit headers:** +```javascript +const remaining = response.headers.get('X-RateLimit-Remaining'); +const type = response.headers.get('X-RateLimit-Type'); + +console.log(`Rate limit remaining: ${remaining} (${type})`); +``` + +## Validation Error Responses + +### Field Validation Errors + +```json +{ + "code": 300, + "message": "Validation Failed.", + "errors": [ + { + "field": "start_time", + "message": "Invalid field." + }, + { + "field": "type", + "message": "Invalid field." + } + ] +} +``` + +**Solution:** +- Check each field's format in API documentation +- Verify required fields are present +- Ensure data types match (number vs string) + +### Common Field Errors + +| Field | Error | Cause | Fix | +|-------|-------|-------|-----| +| `start_time` | Invalid field | Wrong format | Use `yyyy-MM-ddTHH:mm:ssZ` | +| `type` | Invalid field | Invalid meeting type | Use 1, 2, 3, or 8 | +| `email` | Invalid field | Invalid email format | Check email format | +| `duration` | Invalid field | Duration too long | Max duration varies by plan | + +## Error Response Structure + +### Standard Error + +```json +{ + "code": 300, + "message": "Descriptive error message" +} +``` + +### Validation Error + +```json +{ + "code": 300, + "message": "Validation Failed.", + "errors": [ + { + "field": "field_name", + "message": "Error description" + } + ] +} +``` + +## Error Handling Best Practices + +### 1. Parse Error Response + +```javascript +async function handleApiResponse(response) { + if (!response.ok) { + const error = await response.json(); + + console.error(`API Error ${response.status}:`, error); + + // Check for validation errors + if (error.errors && Array.isArray(error.errors)) { + error.errors.forEach(err => { + console.error(`- Field "${err.field}": ${err.message}`); + }); + } + + throw new Error(`${error.code}: ${error.message}`); + } + + return await response.json(); +} +``` + +### 2. Retry with Exponential Backoff + +```javascript +async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + const isRetryable = error.status >= 500 || error.status === 429; + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + const delay = baseDelay * Math.pow(2, attempt - 1); + const jitter = Math.random() * 1000; // Add randomness + + console.log(`Retry attempt ${attempt} after ${delay + jitter}ms`); + await sleep(delay + jitter); + } + } +} +``` + +### 3. Log Errors with Context + +```javascript +function logApiError(error, context) { + const logEntry = { + timestamp: new Date().toISOString(), + status: error.status, + code: error.code, + message: error.message, + context: { + endpoint: context.endpoint, + method: context.method, + userId: context.userId + } + }; + + console.error('API Error:', JSON.stringify(logEntry, null, 2)); + + // Send to monitoring service + // monitoring.track(logEntry); +} +``` + +### 4. User-Friendly Error Messages + +```javascript +function getUserFriendlyError(error) { + const errorMap = { + 1001: 'User not found. Please check the email address.', + 300: 'Invalid request. Please check your input.', + 201: 'Your session has expired. Please sign in again.', + 4700: 'Permission denied. Please contact your administrator.', + 200: 'Too many requests. Please try again in a few moments.' + }; + + return errorMap[error.code] || 'An unexpected error occurred. Please try again.'; +} +``` + +## Troubleshooting Checklist + +### Quick Diagnostic Steps + +1. **Check HTTP status code** - Indicates error category (auth, validation, server) +2. **Read error message** - Provides specific details +3. **Check Zoom error code** - Maps to specific issue +4. **Verify authentication** - Token valid, scopes present +5. **Check rate limits** - Monitor `X-RateLimit-Remaining` header +6. **Test with Postman** - Isolate code vs API issue +7. **Check Zoom Status** - Visit [status.zoom.us](https://status.zoom.us) + +### Common Fixes + +| Symptom | Check | Fix | +|---------|-------|-----| +| 401 Unauthorized | Token expiry | Refresh access token | +| 404 User not found | `me` keyword usage | Use `me` for User OAuth | +| 404 Meeting not found | UUID encoding | Double-encode UUIDs starting with `/` | +| 403 Forbidden | Scopes | Add required scopes in app config | +| 429 Rate limit | Request rate | Implement throttling | +| 400 Validation error | Request body | Check field formats in docs | + +## Related Documentation + +- **[API Architecture](../concepts/api-architecture.md)** - `me` keyword, UUID encoding, time formats +- **[Rate Limiting Strategy](../concepts/rate-limiting-strategy.md)** - Handle 429 errors +- **[Authentication Flows](../concepts/authentication-flows.md)** - Token refresh patterns +- **[Common Issues](common-issues.md)** - Practical troubleshooting + +## Resources + +- [Error Codes Documentation](https://developers.zoom.us/docs/api/errors/) +- [Zoom Status Page](https://status.zoom.us/) +- [Developer Forum](https://devforum.zoom.us/) diff --git a/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-issues.md new file mode 100644 index 00000000..e454ae27 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Common Issues (REST API) + +This complements `common-errors.md` (HTTP codes and Zoom error codes). + +## Pagination + +- Some endpoints use `next_page_token`. +- Others use `page_number` + `page_size`. +- For large accounts, always code defensively for partial results. + +## Token Expiry / Scopes + +- `Access token is expired`: refresh or request a new S2S token. +- `does not contain scopes`: add scopes in Marketplace and re-authorize users (User OAuth). + +## Webhooks + +- Webhooks are at-least-once delivery: design idempotent handlers. +- Verify signatures and handle `endpoint.url_validation`. + +## Also See + +- `common-errors.md` +- `token-scope-playbook.md` + diff --git a/partner-built/zoom-plugin/skills/rest-api/troubleshooting/forum-top-questions.md b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/forum-top-questions.md new file mode 100644 index 00000000..ea59d7dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/forum-top-questions.md @@ -0,0 +1,56 @@ +--- +title: "Forum-Derived Top Questions (REST API)" +--- + +# Forum-Derived Top Questions (REST API) + +This is a checklist of the most common forum questions for **Zoom REST API** integrations. + +## Fast Routing Questions (Ask First) + +- App type: **Server-to-Server OAuth** vs **User OAuth (authorization_code / PKCE)** +- Target endpoints: meetings, webinars, recordings, users, reports, team chat +- Who are you acting as: account-level automation vs per-user actions? +- Exact error: HTTP status + Zoom error `code` + `message` +- Scopes: list current scopes on the token (and whether they match the endpoint) + +## “Invalid access token” / “does not contain scopes” + +Common causes to cover: +- wrong OAuth app type for the use case +- token from the wrong account/app +- scopes not granted or not activated +- using `me` incorrectly (User OAuth vs S2S OAuth rules) + +## Server-to-Server OAuth Migration + +Common asks: +- “How do I switch from JWT app type to S2S OAuth?” +- “What is `grant_type=account_credentials` and `account_id`?” + +Answer pattern: +- explain that Marketplace JWT app type is deprecated (REST API) +- show token request + where to store/refresh tokens server-side + +## Webhooks + +Common asks: +- verification (CRC or event verification) +- “which event proves the user actually joined?” +- limiting events to subsets of users + +Answer pattern: +- recommend event-driven architecture over polling +- show signature/verification steps and typical pitfalls + +## Recordings + Transcripts + +Common asks: +- downloading recordings (`download_url` auth + redirects) +- bulk download across account/users +- getting transcripts and transcription availability + +Answer pattern: +- use webhooks to trigger downloads +- follow redirects and keep auth headers + diff --git a/partner-built/zoom-plugin/skills/rest-api/troubleshooting/token-scope-playbook.md b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/token-scope-playbook.md new file mode 100644 index 00000000..3a8c8be6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rest-api/troubleshooting/token-scope-playbook.md @@ -0,0 +1,48 @@ +--- +title: "Token + Scope Playbook (Invalid Access Token / Missing Scopes)" +--- + +# Token + Scope Playbook (Invalid Access Token / Missing Scopes) + +This covers the most common REST API forum failures: + +- `401` / `403` +- `"Invalid access token"` +- `"does not contain scopes"` +- `"Access token is expired"` + +## Step 1: Identify the OAuth App Type + +- **Server-to-Server OAuth**: backend automation, no user consent screen +- **User OAuth (authorization_code / PKCE)**: per-user actions, user consent + +Wrong app type is the #1 cause of “token works for endpoint A but not endpoint B”. + +## Step 2: Verify `me` Keyword Rules + +- User OAuth: use `users/me/...` +- S2S OAuth: do **not** use `me` (use a real userId/email where required) + +If they get `1001 user does not exist` or “invalid access token” on `users/{id}` calls, this is often the reason. + +## Step 3: Confirm Scopes Match the Endpoint + +If the error says missing scopes, you must: + +1. enable the scopes in Marketplace for the app +2. obtain a **new** token (tokens won’t gain scopes retroactively) + +## Step 4: Token Expiry and Refresh + +- S2S OAuth access tokens expire quickly; refresh on the server and cache with a buffer. +- User OAuth: refresh tokens and retry on `code=201` “Access token is expired.” + +## Step 5: Confirm the Token’s Account/App + +If tokens are created from multiple environments (dev/stage/prod) or accounts, mixups happen. + +Ask for: +- the app type +- the account id (human-readable) +- the exact endpoint being called + diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/rivet-sdk/RUNBOOK.md new file mode 100644 index 00000000..e2fc5def --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/RUNBOOK.md @@ -0,0 +1,70 @@ +# Rivet SDK 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate names against changelog and TypeDoc before release. + +## 1) Confirm Integration Surface + +- Confirm this is a server-side Node.js integration path using `@zoom/rivet`. +- Confirm module boundaries (Chatbot, Team Chat, Meetings, Phone, Accounts, Users, Video SDK). +- Confirm whether the app needs event receiver mode, API-only mode, or both. + +## 2) Confirm Required Credentials + +- `clientId`, `clientSecret` for each module auth flow. +- `webhooksSecretToken` for webhook verification (required for receiver flows). +- `accountId` for S2S modules. +- `redirectUri` and `stateStore` for User OAuth modules. + +## 3) Confirm Lifecycle Order + +1. Instantiate module client(s) with auth/receiver options. +2. Register webhook listeners before traffic. +3. Start client server(s) and note per-module port(s). +4. Expose `/zoom/events` for each active receiver endpoint. +5. Execute API calls through `client.endpoints.*`. + +## 4) Confirm Event/State Handling + +- Match event names exactly (`bot_notification`, `interactive_message_fields_editable`, etc.). +- Keep module-specific webhook endpoints aligned with Marketplace subscription settings. +- Persist OAuth tokens/state for long-lived integrations. +- Keep idempotency in event handlers for retries/duplicates. + +## 5) Confirm Cleanup + Upgrade Posture + +- Revalidate all module constructors and options on each upgrade. +- Keep compatibility shims when module/type names change. +- Review changelog version-by-version from current deployment to target. + +## 6) Quick Probes + +- Client starts and binds to expected port(s). +- Webhook verification succeeds for subscribed events. +- One API call and one event callback complete successfully for each active module. +- User OAuth install/callback flow works end-to-end when applicable. + +## 7) Fast Decision Tree + +- API works but events fail -> wrong endpoint URL/port/path (`/zoom/events`) or bad webhook token. +- OAuth install fails -> redirect URI/state store mismatch or unsupported receiver mode. +- Multi-module collisions -> duplicate ports or shared env keys incorrectly mapped. +- Lambda flow issues -> receiver type mismatch or missing `webhooksSecretToken`. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/rivet/ +- https://developers.zoom.us/docs/rivet/javascript/ +- https://zoom.github.io/rivet-javascript/ + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/` +- `tools/zoom-crawler/raw-docs/zoom.github.io/rivet-javascript/` diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/SKILL.md b/partner-built/zoom-plugin/skills/rivet-sdk/SKILL.md new file mode 100644 index 00000000..fc5880dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/SKILL.md @@ -0,0 +1,99 @@ +--- +name: rivet-sdk +description: "Reference skill for Zoom Rivet SDK. Use after routing to a Rivet-based server workflow when implementing auth handling, webhook consumers, API wrappers, multi-module composition, or Lambda receiver patterns." +user-invocable: false +triggers: + - "rivet" + - "zoom rivet" + - "@zoom/rivet" + - "rivet javascript" + - "rivet sdk" + - "rivet webhook" + - "rivet oauth" + - "rivet lambda" + - "rivet team chat" + - "rivet chatbot" +--- + +# Zoom Rivet SDK + +Background reference for Zoom Rivet as a JavaScript and TypeScript server framework for Zoom integrations. + +Implementation guidance for Zoom Rivet (JavaScript/TypeScript) as a server-side framework for: +- OAuth and token handling +- Webhook event consumption +- Typed REST API endpoint wrappers +- Multi-module server composition + +Official docs: +- https://developers.zoom.us/docs/rivet/ +- https://developers.zoom.us/docs/rivet/javascript/ +- https://zoom.github.io/rivet-javascript/ + +Reference samples: +- https://github.com/zoom/rivet-javascript-sample +- https://github.com/zoom/isv-rivet-starter +- https://github.com/zoom/Rivet-Server-Sample +- https://github.com/zoom/rivet-javascript + +## Routing Guardrail + +- Rivet SDK is a Node.js framework that bundles Zoom auth handling, webhook receivers, and typed API wrappers. +- Rivet is recommended for faster server-side scaffolding, but it is not mandatory. +- At planning start, confirm preference: +- `Do you want Rivet SDK, or direct OAuth + REST without Rivet?` +- Use Rivet when the user wants a Node.js server that combines Zoom auth + webhooks + API calls with minimal glue code. +- If the user only needs direct API calls from an existing backend, chain with [../rest-api/SKILL.md](../rest-api/SKILL.md). +- If the user is focused on Zoom Team Chat app cards/commands behavior, chain with [../team-chat/SKILL.md](../team-chat/SKILL.md). +- If the user needs SDK embed (Meeting SDK/Video SDK client runtime), route to [../meeting-sdk/SKILL.md](../meeting-sdk/SKILL.md) or [../video-sdk/SKILL.md](../video-sdk/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +3. [examples/getting-started-pattern.md](examples/getting-started-pattern.md) +4. [examples/multi-client-pattern.md](examples/multi-client-pattern.md) +5. [references/rivet-reference-map.md](references/rivet-reference-map.md) +6. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +7. [references/samples-validation.md](references/samples-validation.md) +8. [references/source-map.md](references/source-map.md) +9. [references/environment-variables.md](references/environment-variables.md) +10. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) +11. [RUNBOOK.md](RUNBOOK.md) +12. [rivet-sdk.md](rivet-sdk.md) + +## Common Lifecycle Pattern + +1. Choose modules and auth model per module (Client Credentials, User OAuth, S2S OAuth, Video SDK JWT). +2. Instantiate client(s) with credentials, webhook secret, and per-module port. +3. Register event handlers (`webEventConsumer.event(...)` or shortcuts). +4. Implement API calls through `client.endpoints.*`. +5. Start receiver(s) and expose webhook endpoint(s) (`/zoom/events`) to Zoom. +6. Persist tokens/state for OAuth workloads and enforce signature verification. +7. Monitor module-specific failures and rotate secrets/version with changelog cadence. + +## High-Level Scenarios + +- Team Chat slash-command bot + Team Chat data API enrichment. +- Multi-module backend (Users + Meetings + Team Chat + Phone) sharing one process. +- Video SDK telemetry backend using `videosdk` module event stream + API surfaces. +- ISV orchestration layer with tenant-aware token storage and per-module webhooks. +- AWS Lambda webhook processor with Rivet `AwsLambdaReceiver`. + +See [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) for details. + +## Chaining + +- OAuth architecture and grant selection: [../oauth/SKILL.md](../oauth/SKILL.md) +- API endpoint semantics and request payload details: [../rest-api/SKILL.md](../rest-api/SKILL.md) +- Team Chat app cards, command and bot UX: [../team-chat/SKILL.md](../team-chat/SKILL.md) +- Video SDK API-specific behavior and BYOS context: [../video-sdk/SKILL.md](../video-sdk/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/concepts/architecture-and-lifecycle.md b/partner-built/zoom-plugin/skills/rivet-sdk/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..4559fb5a --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/concepts/architecture-and-lifecycle.md @@ -0,0 +1,82 @@ +# Rivet Architecture and Lifecycle + +## What Rivet Provides + +Rivet wraps three concerns into one module client: +- Auth/token orchestration +- Webhook receiver + event dispatch +- Typed REST API endpoint wrappers + +## Architecture Model + +```text ++--------------------+ +------------------------------+ +| Zoom Marketplace | | Your Rivet App | +| App Config | | (Node.js/TypeScript) | ++----------+---------+ +---------------+--------------+ + | | + | OAuth install / token exchange | + |--------------------------------->| + | | + | Webhooks (POST /zoom/events) | + |--------------------------------->| + | v + | +------------------------+ + | | Rivet Module Clients | + | | - ChatbotClient | + | | - TeamChatClient | + | | - Meetings*Client | + | | - Users*Client | + | | - Phone*Client | + | | - VideoSdkClient | + | +-----+------------+-----+ + | | | + | | +--> webEventConsumer + | | + | +--> endpoints.* (REST wrappers) + | | + | v + | +------------------+ + +--------------------------------> | Zoom APIs | + +------------------+ +``` + +## Lifecycle Workflow + +1. Select module(s): +- Example: `ChatbotClient` + `TeamChatClient` for bot + channel lookup. +- Example: `UsersS2SAuthClient` + `MeetingsS2SAuthClient` for admin automation. + +2. Pick auth model by module: +- Chatbot: Client Credentials +- Team Chat/Meetings/Phone/Accounts/Users: User OAuth or S2S OAuth +- Video SDK: JWT auth for Video SDK API + +3. Configure client options: +- Required: `clientId`, `clientSecret` +- Often required: `webhooksSecretToken` +- Conditional: `accountId`, `installerOptions`, `receiver`, `port`, `tokenStore` + +4. Register listeners: +- Generic: `webEventConsumer.event("event_name", handler)` +- Shortcuts where available: `onSlashCommand`, `onButtonClick`, `onChannelMessagePosted` + +5. Start server(s): +- `await client.start()` returns server handler/address +- For multi-module apps, assign unique ports + +6. Wire Marketplace subscriptions: +- Endpoint URL must target each module's receiver port +- Endpoint path should include `/zoom/events` + +7. Process API + events: +- API calls via `client.endpoints.*` +- Event-driven actions via callback handlers + +8. Operate and upgrade: +- Persist OAuth tokens/state for stable restarts +- Use changelog + TypeDoc workflow for version upgrades + +## Why Multi-Client Port Strategy Matters + +In sample patterns, each module runs its own receiver port. If multiple modules share one port by mistake, webhook routing and verification behavior can break in non-obvious ways. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/examples/getting-started-pattern.md b/partner-built/zoom-plugin/skills/rivet-sdk/examples/getting-started-pattern.md new file mode 100644 index 00000000..3446896b --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/examples/getting-started-pattern.md @@ -0,0 +1,43 @@ +# Getting Started Pattern (Single Module) + +This pattern shows a minimal Rivet bootstrap for Team Chat with OAuth installation support. + +```javascript +import { TeamChatClient } from "@zoom/rivet/teamchat"; + +(async () => { + const teamchatClient = new TeamChatClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + installerOptions: { + redirectUri: process.env.RIVET_REDIRECT_URI, + stateStore: process.env.RIVET_STATE_STORE_SECRET, + }, + port: Number(process.env.RIVET_PORT || 8080), + }); + + teamchatClient.webEventConsumer.event("chat_message.sent", ({ payload }) => { + console.log("event", payload); + }); + + const server = await teamchatClient.start(); + console.log("rivet server", server.address()); +})(); +``` + +## Required Marketplace Wiring + +- Add endpoint URL to event subscription with `/zoom/events` suffix. +- Ensure OAuth redirect URI exactly matches `installerOptions.redirectUri` + callback path behavior. +- Include all scopes needed by `endpoints.*` calls. + +## Common Extension + +Add API calls with typed wrappers: + +```javascript +const channels = await teamchatClient.endpoints.chatChannels.listUsersChannels({ + path: { userId: "me" }, +}); +``` diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/examples/multi-client-pattern.md b/partner-built/zoom-plugin/skills/rivet-sdk/examples/multi-client-pattern.md new file mode 100644 index 00000000..d2dfaf6c --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/examples/multi-client-pattern.md @@ -0,0 +1,53 @@ +# Multi-Client Pattern (Two Modules, Two Ports) + +Use this when combining chatbot command handling with Team Chat API lookups. + +```javascript +import { ChatbotClient } from "@zoom/rivet/chatbot"; +import { TeamChatClient } from "@zoom/rivet/teamchat"; + +const CHATBOT_PORT = Number(process.env.RIVET_CHATBOT_PORT || 4001); +const TEAMCHAT_PORT = Number(process.env.RIVET_TEAMCHAT_PORT || 4002); + +(async () => { + const chatbotClient = new ChatbotClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + port: CHATBOT_PORT, + }); + + const teamchatClient = new TeamChatClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + installerOptions: { + redirectUri: process.env.RIVET_REDIRECT_URI, + stateStore: process.env.RIVET_STATE_STORE_SECRET, + }, + port: TEAMCHAT_PORT, + }); + + chatbotClient.webEventConsumer.onSlashCommand("help", async ({ say }) => { + await say("Rivet bot ready."); + }); + + chatbotClient.webEventConsumer.onSlashCommand("channels", async ({ say, payload }) => { + const result = await teamchatClient.endpoints.chatChannels.listUsersChannels({ + path: { userId: payload.userId }, + }); + + const names = (result.data?.channels || []).map((x) => x.name).join(", "); + await say(`Channels: ${names || "none"}`); + }); + + await teamchatClient.start(); + await chatbotClient.start(); +})(); +``` + +## Operational Notes + +- Subscribe chatbot events to `CHATBOT_PORT` endpoint. +- Complete Team Chat OAuth install flow against `TEAMCHAT_PORT`. +- Keep ports unique and explicit in documentation and deployment configs. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/references/environment-variables.md b/partner-built/zoom-plugin/skills/rivet-sdk/references/environment-variables.md new file mode 100644 index 00000000..e4a2afc5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/references/environment-variables.md @@ -0,0 +1,44 @@ +# Rivet SDK Environment Variables + +Standardized `.env` keys for Rivet-based integrations. + +## Core Keys + +| Key | Required | Description | Where to find it | +|-----|----------|-------------|------------------| +| `RIVET_CLIENT_ID` | Yes | OAuth or product client ID used by module | Zoom App Marketplace -> your app -> Basic Information -> Client ID | +| `RIVET_CLIENT_SECRET` | Yes | OAuth or product client secret used by module | Zoom App Marketplace -> your app -> Basic Information -> Client Secret | +| `RIVET_WEBHOOK_SECRET_TOKEN` | Receiver flows | Secret used to verify webhook requests | Zoom App Marketplace -> Feature / Event Subscriptions / Access page -> Secret Token | +| `RIVET_ACCOUNT_ID` | S2S only | Account ID for Server-to-Server OAuth module clients | Zoom App Marketplace -> Server-to-Server OAuth app -> App Credentials | +| `RIVET_REDIRECT_URI` | User OAuth only | Redirect URI for install/callback flow | Zoom App Marketplace -> OAuth settings -> Redirect URL | +| `RIVET_STATE_STORE_SECRET` | User OAuth only | State store signing secret | Generated by you (store in secret manager) | + +## Port Keys + +| Key | Required | Description | +|-----|----------|-------------| +| `RIVET_PORT` | Optional | Single-module HTTP receiver port (default often 8080) | +| `RIVET_CHATBOT_PORT` | Multi-module | Chatbot module port | +| `RIVET_TEAMCHAT_PORT` | Multi-module | Team Chat module port | +| `RIVET_USERS_PORT` | Multi-module | Users module port | +| `RIVET_MEETINGS_PORT` | Multi-module | Meetings module port | +| `RIVET_PHONE_PORT` | Multi-module | Phone module port | +| `RIVET_ACCOUNTS_PORT` | Multi-module | Accounts module port | +| `RIVET_VIDEOSDK_PORT` | Multi-module | Video SDK module port | + +## Video SDK-Specific Note + +Rivet's `videosdk` module constructor uses `clientId`/`clientSecret` fields, but those map to Video SDK credentials for the app type you configure. Validate with current Video SDK credential model before release. + +## Example `.env` + +```env +RIVET_CLIENT_ID="..." +RIVET_CLIENT_SECRET="..." +RIVET_WEBHOOK_SECRET_TOKEN="..." +RIVET_ACCOUNT_ID="..." +RIVET_REDIRECT_URI="http://YOUR_DEV_HOST:4002" +RIVET_STATE_STORE_SECRET="replace-me" +RIVET_CHATBOT_PORT=4001 +RIVET_TEAMCHAT_PORT=4002 +``` diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/references/rivet-reference-map.md b/partner-built/zoom-plugin/skills/rivet-sdk/references/rivet-reference-map.md new file mode 100644 index 00000000..1ada5ef1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/references/rivet-reference-map.md @@ -0,0 +1,41 @@ +# Rivet Reference Map + +## Canonical Documentation + +- Product docs: https://developers.zoom.us/docs/rivet/ +- JavaScript docs: https://developers.zoom.us/docs/rivet/javascript/ +- TypeDoc reference: https://zoom.github.io/rivet-javascript/modules.html + +## Modules (TypeDoc) + +From `@zoom/rivet` TypeDoc module index: +- Accounts +- Chatbot +- Meetings +- Phone +- Team Chat +- Users +- Video SDK + +Each module generally exposes: +- `*Client` class(es) +- `*Endpoints` wrapper class +- `*EventProcessor` +- `HttpReceiver` and `AwsLambdaReceiver` +- Shared option/types/error surfaces + +## Key API Shapes + +- Event subscription: +- `client.webEventConsumer.event(eventName, handler)` +- Event shortcut examples: +- `onSlashCommand`, `onButtonClick`, `onChannelMessagePosted` +- Endpoint wrappers: +- `client.endpoints..({ path, query, body })` + +## Important Links for Validation + +- Rivet sample app: https://github.com/zoom/rivet-javascript-sample +- ISV starter sample: https://github.com/zoom/isv-rivet-starter +- Rivet server sample: https://github.com/zoom/Rivet-Server-Sample +- Rivet package source: https://github.com/zoom/rivet-javascript diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/references/samples-validation.md b/partner-built/zoom-plugin/skills/rivet-sdk/references/samples-validation.md new file mode 100644 index 00000000..c6ac9201 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/references/samples-validation.md @@ -0,0 +1,41 @@ +# Rivet Sample Validation and Observed Drift + +Validated sources: +- `zoom/rivet-javascript-sample` +- `zoom/isv-rivet-starter` +- `zoom/Rivet-Server-Sample` +- `zoom/rivet-javascript` + +## Lifecycle Patterns Confirmed + +- Module clients are instantiated with auth + receiver options. +- Handlers are registered before or near startup. +- `client.start()` bootstraps receiver/server. +- Multi-module samples use unique ports per module. +- `/zoom/events` endpoint suffix is required for webhook callbacks. + +## Architecture Patterns Confirmed + +- Rivet acts as an orchestration layer: +- Typed endpoint wrappers for API operations. +- Webhook consumer methods for events. +- OAuth helper behavior embedded in client lifecycle. + +## Useful Additions Incorporated into Skill + +- Multi-module port segregation and webhook endpoint mapping. +- Distinct auth patterns by module. +- Sample-derived operational gotchas for ngrok and OAuth install. + +## Contradictions and Drift Notes + +- Some docs/samples reference older Team Chat doc paths (`team-chat-apps`) while current docs may use updated routing. +- Sample env variable naming is inconsistent across repos (`StS_*`, `WEBHOOK_SECRET_TOKEN`, per-module keys). This skill standardizes names in `environment-variables.md`. +- Some sample README commands imply one port while module receivers may actually use `base+1` or per-module ports. +- User OAuth behavior depends on receiver choice; `AwsLambdaReceiver` limitations must be handled explicitly. + +## Recommendations + +- Keep a local compatibility table: `rivet_version` x `modules_used` x `auth_flows` x `receiver_type`. +- Treat sample repos as patterns, not strict source of truth. +- Re-check TypeDoc and changelog before each release. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/references/source-map.md b/partner-built/zoom-plugin/skills/rivet-sdk/references/source-map.md new file mode 100644 index 00000000..e9123959 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/references/source-map.md @@ -0,0 +1,19 @@ +# Rivet Source Map + +## Crawled Docs (local raw-docs) + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/get-started.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/authorization.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/apis-events.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/config-options.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/deployment.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/event-shortcuts.md` +- `tools/zoom-crawler/raw-docs/zoom.github.io/rivet-javascript/` (TypeDoc crawl) + +## External Validation Repos + +- `zoom/rivet-javascript-sample` +- `zoom/isv-rivet-starter` +- `zoom/Rivet-Server-Sample` +- `zoom/rivet-javascript` diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/rivet-sdk/references/versioning-and-compatibility.md new file mode 100644 index 00000000..42bc7f50 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/references/versioning-and-compatibility.md @@ -0,0 +1,32 @@ +# Rivet Versioning and Compatibility + +## Upgrade Strategy + +Use the standard upgrade workflow: +- [../../general/references/sdk-upgrade-workflow.md](../../general/references/sdk-upgrade-workflow.md) + +For Rivet, treat upgrades as three parallel checks: +1. `@zoom/rivet` package release changes +2. Underlying Zoom API/event payload changes +3. Marketplace app config and scope changes + +## Compatibility Risks + +- Module/auth behavior drift across versions. +- Type alias or endpoint wrapper signature changes. +- Event payload shape differences for webhook types. +- Receiver behavior changes in Node runtime/Lambda environments. + +## Version Signals + +- `@zoom/rivet` package version in `package.json`. +- TypeDoc module/classes/type aliases under `zoom.github.io/rivet-javascript`. +- Changelog updates under https://developers.zoom.us/changelog/. + +## Safe Upgrade Checklist + +- Pin current and target `@zoom/rivet` versions. +- Compare TypeDoc module pages for changed constructor options and endpoints. +- Validate event names and payload fields used by your handlers. +- Revalidate OAuth install/callback flow and token persistence. +- Revalidate per-module port and `/zoom/events` mapping. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/rivet-sdk.md b/partner-built/zoom-plugin/skills/rivet-sdk/rivet-sdk.md new file mode 100644 index 00000000..49a1e88a --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/rivet-sdk.md @@ -0,0 +1,15 @@ +# Zoom Rivet SDK (Overview) + +Rivet is a server-side framework for Zoom integrations in JavaScript/TypeScript. + +For full documentation and navigation, start at [SKILL.md](SKILL.md). + +## Quick Links + +- [Architecture and Lifecycle](concepts/architecture-and-lifecycle.md) +- [High-Level Scenarios](scenarios/high-level-scenarios.md) +- [Getting Started Pattern](examples/getting-started-pattern.md) +- [Multi-Client Pattern](examples/multi-client-pattern.md) +- [Reference Map](references/rivet-reference-map.md) +- [Sample Validation](references/samples-validation.md) +- [Common Issues](troubleshooting/common-issues.md) diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/rivet-sdk/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..e1883dc4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/scenarios/high-level-scenarios.md @@ -0,0 +1,60 @@ +# Rivet High-Level Scenarios + +## 1) Team Chat Standup Bot with Channel Intelligence + +- Modules: `ChatbotClient` + `TeamChatClient` +- Auth: Client Credentials (chatbot) + User OAuth or S2S for Team Chat APIs +- Flow: +1. Slash command enters chatbot webhook. +2. Bot queries channel and member lists via Team Chat endpoints. +3. Bot posts/updates interactive message cards. +- Risks: +- Misaligned scopes cause endpoint failures. +- Wrong port in Marketplace event subscription prevents callbacks. + +## 2) ISV Admin Automation Service + +- Modules: `UsersS2SAuthClient`, `MeetingsS2SAuthClient`, optionally `AccountsS2SAuthClient` +- Auth: S2S OAuth +- Flow: +1. Backend receives internal request to create/update Zoom resources. +2. Rivet endpoints wrap REST calls. +3. Webhooks confirm completion state. +- Risks: +- Missing `accountId` or stale S2S credentials. +- Event and API schema mismatch across versions. + +## 3) Video SDK API Operations and Recording Workflow + +- Module: `VideoSdkClient` +- Auth: Video SDK JWT +- Flow: +1. Create/list/manage sessions. +2. Manage recording/BYOS/report endpoints. +3. React to session/recording webhooks. +- Risks: +- Wrong credential type (OAuth client vs Video SDK key/secret). +- Ignoring recording and BYOS endpoint field changes after upgrades. + +## 4) AWS Lambda Event Receiver Deployment + +- Module: product-specific client + `AwsLambdaReceiver` +- Flow: +1. Instantiate client with `receiver: new AwsLambdaReceiver(...)`. +2. Export Lambda handler that delegates to `await client.start()` handler. +3. Use API Gateway/serverless-offline for local parity. +- Risks: +- User OAuth expectations with unsupported receiver flow. +- Secret token mismatch across Lambda environments. + +## 5) Multi-Tenant Event Router + +- Modules: one or more, with external token/state stores +- Flow: +1. Receive webhook. +2. Resolve tenant and token context. +3. Execute routed API action. +4. Persist audit and retry state. +- Risks: +- In-memory token store only (tokens lost on restart). +- Missing idempotency for retried webhook events. diff --git a/partner-built/zoom-plugin/skills/rivet-sdk/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/rivet-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..71a47cdd --- /dev/null +++ b/partner-built/zoom-plugin/skills/rivet-sdk/troubleshooting/common-issues.md @@ -0,0 +1,51 @@ +# Rivet Common Issues + +## Webhooks never reach handlers + +Symptoms: +- `client.start()` succeeds, but no event callbacks fire. + +Checks: +- Marketplace endpoint URL points to correct module port. +- Endpoint includes `/zoom/events` suffix. +- `webhooksSecretToken` matches app configuration. +- Ngrok forward is active and mapped to the receiver port. + +## OAuth install/callback fails + +Symptoms: +- Install page redirects fail or callback errors. + +Checks: +- `installerOptions.redirectUri` exactly matches Marketplace OAuth redirect. +- `stateStore` value is configured and stable. +- Receiver mode supports User OAuth (Lambda receiver caveat). + +## Multi-module runtime conflicts + +Symptoms: +- One module works, another silently fails. + +Checks: +- Every module uses a unique port. +- Event subscriptions target the correct module endpoint. +- Env variables are not accidentally shared with wrong module. + +## API-only mode confusion + +Symptoms: +- OAuth expectations fail when receiver disabled. + +Checks: +- `disableReceiver: true` disables OAuth flow behavior. +- For OAuth + API-only behavior, use receiver-compatible configuration and relax webhook verification as documented. + +## Sample parity mismatches + +Symptoms: +- Following sample exactly still fails in your environment. + +Checks: +- Normalize env key names to your project standard. +- Reconcile sample README assumptions with current TypeDoc signatures. +- Verify module auth type alignment (Client Credentials vs User OAuth vs S2S). diff --git a/partner-built/zoom-plugin/skills/rtms/RUNBOOK.md b/partner-built/zoom-plugin/skills/rtms/RUNBOOK.md new file mode 100644 index 00000000..ee6314a5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/RUNBOOK.md @@ -0,0 +1,83 @@ +# RTMS 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the highest-frequency RTMS issues fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Architecture Assumption + +- RTMS is backend-first media ingestion. +- Frontend is optional and should consume backend outputs (WebSocket/SSE/etc). + +If implementation assumes frontend-only RTMS behavior, redesign first. + +## 2) Confirm Event-Triggered Kickoff + +- Processing starts only after RTMS lifecycle start events: + - `meeting.rtms_started` + - `webinar.rtms_started` + - `session.rtms_started` +- Stop events should deactivate pipeline. + +If media handling starts before lifecycle start, session gating is wrong. + +## 3) Confirm Product-Specific IDs + +- Meetings/Webinars: use `meeting_uuid` +- Video SDK: use `session_id` +- Use `rtms_stream_id` from payload for stream context + +Using wrong ID field commonly breaks handshake/signature. + +## 4) Confirm Webhook Handling Pattern + +- Respond `200` immediately. +- Do heavy work asynchronously. +- Verify webhook signature if secret token is configured. + +Slow webhook responses can trigger retries and duplicate stream attempts. + +## 5) Confirm Connection and Heartbeat + +- Track one active connection per stream/session reference. +- Handle heartbeat ping/pong per protocol. +- Implement reconnection strategy explicitly. + +No heartbeat handling means unexpected disconnects. + +## 6) Confirm Media Subscription/Gating + +- Ensure requested media types match your processing path. +- Reject/ignore media packets for inactive sessions. +- Expose pipeline status endpoint for observability. + +This avoids silent packet handling when lifecycle is not active. + +## 7) Quick Probe Checklist + +- `GET /api/health` returns service alive. +- `GET /api/pipeline/status` shows expected active session count. +- Mock/media probes show: + - media before start -> rejected + - start event -> pipeline active + - media after start -> accepted + +### Copy/Paste Validation Commands + +```bash +curl -sS "$RTMS_BASE_URL/api/health" +curl -sS "$RTMS_BASE_URL/api/pipeline/status" +``` + +Expected: healthy service JSON and correct active pipeline visibility. + +## 8) Fast Decision Tree + +- **No media at all** -> lifecycle event not received or wrong webhook route. +- **Duplicate streams** -> delayed webhook response or no active-session guard. +- **Handshake/auth errors** -> wrong credential pair or wrong session ID field. +- **Frontend appears idle** -> backend bridge not connected, not an RTMS source issue. diff --git a/partner-built/zoom-plugin/skills/rtms/SKILL.md b/partner-built/zoom-plugin/skills/rtms/SKILL.md new file mode 100644 index 00000000..d594a152 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/SKILL.md @@ -0,0 +1,580 @@ +--- +name: zoom-rtms +description: Reference skill for Zoom RTMS. Use after routing to a live-media workflow when processing real-time audio, video, chat, transcripts, screen share, or contact-center voice streams. +user-invocable: false +triggers: + - "real-time media" + - "rtms" + - "live audio stream" + - "live video stream" + - "meeting transcription" + - "raw audio" + - "raw video" + - "websocket media" + - "live transcript" + - "streaming audio" + - "streaming video" + - "meeting bot media" + - "contact center voice media" + - "participant video on" + - "participant video off" + - "single individual video stream" +--- + +# Zoom Realtime Media Streams (RTMS) + +Background reference for live Zoom media pipelines. Prefer `build-zoom-bot` first, then use this skill for stream types, capabilities, and RTMS-specific implementation constraints. + +# Zoom Realtime Media Streams (RTMS) + +Expert guidance for accessing live audio, video, transcript, chat, and screen share data from Zoom meetings, webinars, Video SDK sessions, and Zoom Contact Center Voice in real-time. RTMS uses a WebSocket-based protocol with open standards and does not require a meeting bot to capture the media plane. + +## Read This First (Critical) + +RTMS is primarily a **backend media ingestion service**. + +- Your backend receives and processes live media: **audio, video, screen share, chat, transcript**. +- RTMS is not a frontend UI SDK by itself. +- Processing is **event-triggered**: backend waits for RTMS start webhook events before stream handling begins. + +Optional architecture (common): + +- Add a **Zoom App SDK** frontend for in-client UI/controls. +- Stream backend RTMS outputs to frontend via **WebSocket** (or SSE, gRPC, queue workers, etc.). + +Use RTMS for media/data plane, and use frontend frameworks/Zoom Apps for presentation + user interactions. + +**Official Documentation**: https://developers.zoom.us/docs/rtms/ +**SDK Reference (JS)**: https://zoom.github.io/rtms/js/ +**SDK Reference (Python)**: https://zoom.github.io/rtms/py/ +**Sample Repository**: https://github.com/zoom/rtms-samples + +## Quick Links + +**New to RTMS? Follow this path:** + +1. **[Connection Architecture](concepts/connection-architecture.md)** - Two-phase WebSocket design +2. **[SDK Quickstart](examples/sdk-quickstart.md)** - Fastest way to receive media (recommended) +3. **[Manual WebSocket](examples/manual-websocket.md)** - Full protocol control without SDK +4. **[Media Types](references/media-types.md)** - Audio, video, transcript, chat, screen share + +**Complete Implementation:** +- **[RTMS Bot](examples/rtms-bot.md)** - End-to-end bot implementation guide + +**Reference:** +- **[Lifecycle Flow](concepts/lifecycle-flow.md)** - Complete webhook-to-streaming flow +- **[Data Types](references/data-types.md)** - All enums and constants +- **[Webhooks](references/webhooks.md)** - Event subscription details +- **[Environment Variables](references/environment-variables.md)** - credential modes and runtime knobs +- **[Quickstart Notes](references/quickstart.md)** - Secondary quickstart guide +- **Integrated Index** - see the section below in this file + +**Having issues?** +- Connection fails -> [Common Issues](troubleshooting/common-issues.md) +- Duplicate connections -> [Webhook Gotchas](troubleshooting/common-issues.md#webhook-response-timing) +- No audio/video -> [Media Configuration](references/media-types.md) +- Start with preflight checks -> [5-Minute Runbook](RUNBOOK.md) + +## Supported Products + +| Product | Webhook Event | Payload ID | App Type | +|---------|--------------|------------|----------| +| **Meetings** | `meeting.rtms_started` / `meeting.rtms_stopped` | `meeting_uuid` | General App | +| **Webinars** | `webinar.rtms_started` / `webinar.rtms_stopped` | `meeting_uuid` (same!) | General App | +| **Video SDK** | `session.rtms_started` / `session.rtms_stopped` | `session_id` | Video SDK App | +| **Zoom Contact Center Voice** | Product-specific RTMS/ZCC Voice events | Product-specific stream/session identifiers | Contact Center / approved RTMS integration | + +Once connected, the core signaling/media socket model is shared across products. Meetings, webinars, and Video SDK sessions use the familiar start/stop webhooks. Zoom Contact Center Voice adds its own RTMS/ZCC Voice event family and should be treated as the same transport model with product-specific event payloads. + +## RTMS Overview + +RTMS is a data pipeline that gives your app access to live media from Zoom meetings, webinars, and Video SDK sessions **without participant bots**. Instead of having automated clients join meetings, use RTMS to collect media data directly from Zoom's infrastructure. + +### What RTMS Provides + +| Media Type | Format | Use Cases | +|------------|--------|-----------| +| **Audio** | PCM (L16), G.711, G.722, Opus | Transcription, voice analysis, recording | +| **Video** | H.264, JPG, PNG | Recording, AI vision, thumbnails, active participant selection | +| **Screen Share** | H.264, JPG, PNG | Content capture, slide extraction | +| **Transcript** | JSON text | Meeting notes, search, compliance | +| **Chat** | JSON text | Archive, sentiment analysis | + +### March 2026 Protocol Changes + +- **Zoom Contact Center Voice support**: RTMS now covers Contact Center Voice audio and transcript scenarios. +- **Transcript Language Identification control**: transcript media handshakes now support `src_language` and `enable_lid`. Default behavior is LID enabled. Set `enable_lid: false` to force a fixed language. +- **Single individual video stream subscription**: RTMS can now stream one participant's camera feed at a time when `data_opt` is set to `VIDEO_SINGLE_INDIVIDUAL_STREAM`. +- **Graceful client-initiated shutdown**: backends can send `STREAM_CLOSE_REQ` over the signaling socket and wait for `STREAM_CLOSE_RESP`. +- **Media keep-alive tolerance increased**: media socket keep-alive timeout is now **65 seconds**, not 35. + +### Two Approaches + +| Approach | Best For | Complexity | +|----------|----------|------------| +| **SDK** (`@zoom/rtms`) | Most use cases | Low - handles WebSocket complexity | +| **Manual WebSocket** | Custom protocols, other languages | High - full protocol implementation | + +## Prerequisites + +- **Node.js 20.3.0+** (24 LTS recommended) for JavaScript SDK +- **Python 3.10+** for Python SDK +- Zoom General App (for meetings/webinars) or Video SDK App (for Video SDK) with RTMS feature enabled +- Webhook endpoint for RTMS events +- Server to receive WebSocket streams + +> **Need RTMS access?** Post in [Zoom Developer Forum](https://devforum.zoom.us/) requesting RTMS access with your use case. + +## Quick Start (SDK - Recommended) + +```javascript +import rtms from "@zoom/rtms"; + +// All RTMS start/stop events across products +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Handle webhook events +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onAudioData((data, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}: ${data.length} bytes`); + }); + + client.onTranscriptData((data, timestamp, metadata) => { + const text = data.toString('utf8'); + console.log(`${metadata.userName}: ${text}`); + }); + + client.onJoinConfirm((reason) => { + console.log(`Joined session: ${reason}`); + }); + + // SDK handles all WebSocket connections automatically + // Accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Quick Start (Manual WebSocket) + +For full control or non-SDK languages, implement the two-phase WebSocket protocol: + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; + +// 1. Generate signature +// For meetings/webinars: uses meeting_uuid. For Video SDK: uses session_id. +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret).update(message).digest('hex'); +} + +// 2. Handle webhook +app.post('/webhook', (req, res) => { + res.status(200).send(); // CRITICAL: Respond immediately! + + const { event, payload } = req.body; + if (RTMS_EVENTS.includes(event)) { + connectToRTMS(payload); + } +}); + +// 3. Connect to signaling WebSocket +function connectToRTMS(payload) { + const { server_urls, rtms_stream_id } = payload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = payload.meeting_uuid || payload.session_id; + const signature = generateSignature(CLIENT_ID, idValue, rtms_stream_id, CLIENT_SECRET); + + const signalingWs = new WebSocket(server_urls); + + signalingWs.on('open', () => { + signalingWs.send(JSON.stringify({ + msg_type: 1, // Handshake request + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id, + signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + // ... handle responses, connect to media WebSocket +} +``` + +**See**: [Manual WebSocket Guide](examples/manual-websocket.md) for complete implementation. + +## Media Type Bitmask + +Combine types with bitwise OR: + +| Type | Value | Description | +|------|-------|-------------| +| Audio | 1 | PCM audio samples | +| Video | 2 | H.264/JPG video frames | +| Screen Share | 4 | **Separate from video!** | +| Transcript | 8 | Real-time speech-to-text | +| Chat | 16 | In-meeting chat messages | +| All | 32 | All media types | + +**Example**: Audio + Transcript = `1 | 8` = `9` + +## Critical Gotchas + +| Issue | Solution | +|-------|----------| +| **Only 1 connection allowed** | New connections kick out existing ones. Track active sessions! | +| **Respond 200 immediately** | If webhook delays, Zoom retries creating duplicate connections | +| **Heartbeat mandatory** | Respond to msg_type 12 with msg_type 13, or connection dies | +| **Reconnection is YOUR job** | RTMS doesn't auto-reconnect. Media keep-alive tolerance is now about **65s**; signaling remains around **60s** | +| **Transcript language drift** | Use `src_language` plus `enable_lid: false` when you want fixed-language transcription instead of automatic language switching | +| **Single participant video only** | `VIDEO_SINGLE_INDIVIDUAL_STREAM` supports one participant at a time. A new `VIDEO_SUBSCRIPTION_REQ` overrides the previous selection | +| **Graceful close is explicit now** | Use `STREAM_CLOSE_REQ` / `STREAM_CLOSE_RESP` when your backend wants to terminate the stream cleanly | + +## Environment Variables + +### SDK Environment Variables + +```bash +# Required - Authentication +ZM_RTMS_CLIENT=your_client_id # Zoom OAuth Client ID +ZM_RTMS_SECRET=your_client_secret # Zoom OAuth Client Secret + +# Optional - Webhook server +ZM_RTMS_PORT=8080 # Default: 8080 +ZM_RTMS_PATH=/webhook # Default: / + +# Optional - Logging +ZM_RTMS_LOG_LEVEL=info # error, warn, info, debug, trace +ZM_RTMS_LOG_FORMAT=progressive # progressive or json +ZM_RTMS_LOG_ENABLED=true +``` + +### Manual Implementation Variables + +```bash +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret +ZOOM_SECRET_TOKEN=your_webhook_token # For webhook validation +``` + +## Zoom App Setup + +### For Meetings and Webinars (General App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) -> Develop -> Build App +2. Choose **General App** -> **User-Managed** +3. Features -> Access -> **Enable Event Subscription** +4. Add Events -> Search "rtms" -> Select: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +5. Scopes -> Add Scopes -> Search "rtms" -> Add: + - `meeting:read:meeting_audio` + - `meeting:read:meeting_video` + - `meeting:read:meeting_transcript` + - `meeting:read:meeting_chat` + - `webinar:read:webinar_audio` (if using webinars) + - `webinar:read:webinar_video` (if using webinars) + - `webinar:read:webinar_transcript` (if using webinars) + - `webinar:read:webinar_chat` (if using webinars) + +### For Video SDK (Video SDK App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) -> Develop -> Build App +2. Choose **Video SDK App** +3. Use your SDK Key and SDK Secret (not OAuth Client ID/Secret) +4. Add Events: + - `session.rtms_started` + - `session.rtms_stopped` + +## Sample Repositories + +### Official Samples + +| Repository | Description | +|------------|-------------| +| [rtms-samples](https://github.com/zoom/rtms-samples) | RTMSManager, boilerplates, AI samples | +| [rtms-quickstart-js](https://github.com/zoom/rtms-quickstart-js) | JavaScript SDK quickstart | +| [rtms-quickstart-py](https://github.com/zoom/rtms-quickstart-py) | Python SDK quickstart | +| [rtms-sdk-cpp](https://github.com/zoom/rtms-sdk-cpp) | C++ SDK | +| [zoom-rtms](https://github.com/zoom/rtms) | Main SDK repository | + +### AI Integration Samples + +| Sample | Description | +|--------|-------------| +| [rtms-meeting-assistant-starter-kit](https://github.com/zoom/rtms-meeting-assistant-starter-kit) | AI meeting assistant with summaries | +| [arlo-meeting-assistant](https://github.com/zoom/arlo-meeting-assistant) | Production meeting assistant with DB | +| [videosdk-rtms-transcribe-audio](https://github.com/zoom/videosdk-rtms-transcribe-audio) | Whisper transcription | + +## Complete Documentation + +### Concepts +- **[Connection Architecture](concepts/connection-architecture.md)** - Two-phase WebSocket design +- **[Lifecycle Flow](concepts/lifecycle-flow.md)** - Webhook to streaming flow + +### Examples +- **[SDK Quickstart](examples/sdk-quickstart.md)** - Using @zoom/rtms SDK +- **[Manual WebSocket](examples/manual-websocket.md)** - Raw protocol implementation +- **[RTMS Bot](examples/rtms-bot.md)** - Complete bot implementation guide +- **[AI Integration](examples/ai-integration.md)** - Transcription and analysis patterns + +### References +- **[Media Types](references/media-types.md)** - Audio, video, transcript, chat, screen share +- **[Data Types](references/data-types.md)** - All enums and constants +- **[Connection](references/connection.md)** - WebSocket protocol details +- **[Webhooks](references/webhooks.md)** - Event subscription + +### Troubleshooting +- **[Common Issues](troubleshooting/common-issues.md)** - FAQ and solutions + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/rtms/ +- **Data types**: https://developers.zoom.us/docs/rtms/data-types/ +- **Media params**: https://developers.zoom.us/docs/rtms/media-parameter-definition/ +- **Developer forum**: https://devforum.zoom.us/ + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +RTMS provides real-time access to live audio, video, transcript, chat, and screen share from Zoom meetings, webinars, and Video SDK sessions. + +## Critical Positioning + +Treat RTMS as a **backend service** for receiving and processing media streams. + +- Backend role: ingest audio/video/share/chat/transcript, run AI/analytics, persist/forward data. +- Optional frontend role: Zoom App SDK or web dashboard that consumes processed stream data from backend transport (WebSocket/SSE/other). +- Kickoff model: backend waits for RTMS start webhook events, then starts stream processing. + +Do not model RTMS as a frontend-only SDK. + +## Quick Start Path + +**If you're new to RTMS, follow this order:** + +1. **Run preflight checks first** -> [RUNBOOK.md](RUNBOOK.md) +2. **Understand the architecture** -> [concepts/connection-architecture.md](concepts/connection-architecture.md) + - Two-phase WebSocket: Signaling + Media + - Why RTMS doesn't use bots + +3. **Choose your approach** -> SDK or Manual + - SDK (recommended): [examples/sdk-quickstart.md](examples/sdk-quickstart.md) + - Manual WebSocket: [examples/manual-websocket.md](examples/manual-websocket.md) + +4. **Understand the lifecycle** -> [concepts/lifecycle-flow.md](concepts/lifecycle-flow.md) + - Webhook -> Signaling -> Media -> Streaming + +5. **Configure media types** -> [references/media-types.md](references/media-types.md) + - Audio, video, transcript, chat, screen share + +6. **Troubleshoot issues** -> [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Connection problems, duplicate webhooks, missing data + +--- + +## Documentation Structure + +``` +rtms/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── connection-architecture.md # Two-phase WebSocket design +│ └── lifecycle-flow.md # Webhook to streaming flow +│ +├── examples/ # Complete working code +│ ├── sdk-quickstart.md # Using @zoom/rtms SDK +│ ├── manual-websocket.md # Raw protocol implementation +│ ├── rtms-bot.md # Complete RTMS bot implementation +│ └── ai-integration.md # Transcription and analysis +│ +├── references/ # Reference documentation +│ ├── media-types.md # Audio, video, transcript, chat, share +│ ├── data-types.md # All enums and constants +│ ├── connection.md # WebSocket protocol details +│ └── webhooks.md # Event subscription +│ +└── troubleshooting/ # Problem solving guides + └── common-issues.md # FAQ and solutions +``` + +--- + +## By Use Case + +### I want to get meeting transcripts +1. [SDK Quickstart](examples/sdk-quickstart.md) - Fastest approach +2. [Media Types](references/media-types.md#transcript) - Transcript configuration +3. [AI Integration](examples/ai-integration.md) - Whisper, Deepgram, AssemblyAI + +### I want to record meetings +1. [Media Types](references/media-types.md) - Audio + Video configuration +2. [SDK Quickstart](examples/sdk-quickstart.md) - Receiving media +3. [AI Integration](examples/ai-integration.md#audio-recording) - Gap-filled recording + +### I want to build an AI meeting assistant +1. [AI Integration](examples/ai-integration.md) - Complete patterns +2. [SDK Quickstart](examples/sdk-quickstart.md) - Media ingestion +3. [Lifecycle Flow](concepts/lifecycle-flow.md) - Event handling + +### I want to build a complete RTMS bot +1. [RTMS Bot](examples/rtms-bot.md) - **Complete implementation guide** +2. [Lifecycle Flow](concepts/lifecycle-flow.md) - Webhook to streaming flow +3. [Connection Architecture](concepts/connection-architecture.md) - Two-phase design + +### I need full protocol control +1. [Manual WebSocket](examples/manual-websocket.md) - **START HERE** +2. [Connection Architecture](concepts/connection-architecture.md) - Two-phase design +3. [Data Types](references/data-types.md) - All message types and enums +4. [Connection](references/connection.md) - Protocol details + +### I'm getting connection errors +1. [Common Issues](troubleshooting/common-issues.md) - Diagnostic checklist +2. [Connection Architecture](concepts/connection-architecture.md) - Verify flow +3. [Webhooks](references/webhooks.md) - Validation and timing + +### I want to understand the architecture +1. [Connection Architecture](concepts/connection-architecture.md) - Two-phase WebSocket +2. [Lifecycle Flow](concepts/lifecycle-flow.md) - Complete flow diagram +3. [Data Types](references/data-types.md) - Protocol constants + +--- + +## By Product + +### I'm building for Zoom Meetings +- Standard RTMS setup. Webhook event: `meeting.rtms_started`. Uses General App with OAuth. +- Start with [SDK Quickstart](examples/sdk-quickstart.md) or [Manual WebSocket](examples/manual-websocket.md). + +### I'm building for Zoom Webinars +- Same as meetings, but webhook event is `webinar.rtms_started`. Payload still uses `meeting_uuid` (NOT `webinar_uuid`). +- Add webinar scopes and event subscriptions. See [Webhooks](references/webhooks.md). +- Only **panelist** streams are confirmed available. Attendee streams may not be individual. + +### I'm building for Zoom Video SDK +- Webhook event: `session.rtms_started`. Payload uses `session_id` (NOT `meeting_uuid`). +- Requires a **Video SDK App** with SDK Key/Secret (not OAuth Client ID/Secret). +- Once connected, the protocol is **identical** to meetings. +- See [Webhooks](references/webhooks.md) for payload details. + +--- + +## Key Documents + +### 1. Connection Architecture (CRITICAL) +**[concepts/connection-architecture.md](concepts/connection-architecture.md)** + +RTMS uses **two separate WebSocket connections**: +- **Signaling WebSocket**: Authentication, control, heartbeats +- **Media WebSocket**: Actual audio/video/transcript data + +### 2. SDK vs Manual (DECISION POINT) +**[examples/sdk-quickstart.md](examples/sdk-quickstart.md)** vs **[examples/manual-websocket.md](examples/manual-websocket.md)** + +| SDK | Manual | +|-----|--------| +| Handles WebSocket complexity | Full protocol control | +| Automatic reconnection | DIY reconnection | +| Less code | More code | +| Best for most use cases | Best for custom requirements | + +### 3. Critical Gotchas (MOST COMMON ISSUES) +**[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** + +1. **Respond 200 immediately** - Delayed webhook responses cause duplicates +2. **Only 1 connection per stream** - New connections kick out existing +3. **Heartbeat required** - Must respond to keep-alive or connection dies +4. **Track active sessions** - Prevent duplicate join attempts + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Two-Phase WebSocket Design** + - Signaling: Control plane (handshake, heartbeat, start/stop) + - Media: Data plane (audio, video, transcript, chat, share) + - See: [Connection Architecture](concepts/connection-architecture.md) + +2. **Webhook Response Timing** + - MUST respond 200 BEFORE any processing + - Delayed response -> Zoom retries -> duplicate connections + - See: [Common Issues](troubleshooting/common-issues.md#webhook-response-timing) + +3. **Heartbeat is Mandatory** + - Signaling: Receive msg_type 12, respond with msg_type 13 + - Media: Same pattern + - Failure to respond = connection closed + - See: [Connection](references/connection.md#heartbeat) + +4. **Signature Generation** + - Format: `HMAC-SHA256(clientSecret, "clientId,meetingUuid,streamId")` + - For Video SDK, use `session_id` in place of `meetingUuid` + - Webinars still use `meeting_uuid` (not `webinar_uuid`) + - Required for both signaling and media handshakes + - See: [Manual WebSocket](examples/manual-websocket.md#signature-generation) + +5. **Media Types are Bitmasks** + - Audio=1, Video=2, Share=4, Transcript=8, Chat=16, All=32 + - Combine with OR: Audio+Transcript = 1|8 = 9 + - See: [Media Types](references/media-types.md) + +6. **Screen Share is SEPARATE from Video** + - Different msg_type (16 vs 15) + - Different media flag (4 vs 2) + - Must subscribe separately + - See: [Media Types](references/media-types.md#screen-share) + +--- + +## Quick Reference + +### "Connection fails" +-> [Common Issues](troubleshooting/common-issues.md) + +### "Duplicate connections" +-> [Webhook timing](troubleshooting/common-issues.md#webhook-response-timing) + +### "No audio/video data" +-> [Media Types](references/media-types.md) - Check configuration + +### "How do I implement manually?" +-> [Manual WebSocket](examples/manual-websocket.md) + +### "What message types exist?" +-> [Data Types](references/data-types.md) + +### "How do I integrate AI?" +-> [AI Integration](examples/ai-integration.md) + +--- + +## Document Version + +Based on **Zoom RTMS SDK v1.x** and official documentation as of 2026. + +--- + +**Happy coding!** + +Remember: Start with [SDK Quickstart](examples/sdk-quickstart.md) for the fastest path, or [Manual WebSocket](examples/manual-websocket.md) if you need full control. diff --git a/partner-built/zoom-plugin/skills/rtms/concepts/connection-architecture.md b/partner-built/zoom-plugin/skills/rtms/concepts/connection-architecture.md new file mode 100644 index 00000000..87d5fe74 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/concepts/connection-architecture.md @@ -0,0 +1,223 @@ +# RTMS Connection Architecture + +RTMS uses a **two-phase WebSocket design** to separate control plane from data plane. + +## Overview + +> **Multi-Product Note**: The two-phase WebSocket design described here is **identical** for all RTMS products (meetings, webinars, and Video SDK sessions). The only difference is the initial webhook event name and payload ID field. Once connected, the signaling and media protocols are the same. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Zoom Meeting │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Zoom RTMS Backend │ +│ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Signaling Server │ │ Media Server │ │ +│ │ (Control Plane) │ │ (Data Plane) │ │ +│ └──────────┬──────────┘ └──────────────┬──────────────┘ │ +└─────────────┼───────────────────────────────┼───────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Your Server │ +│ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Signaling Socket │ │ Media Socket │ │ +│ │ - Handshake │ │ - Audio data │ │ +│ │ - Start/Stop │ │ - Video data │ │ +│ │ - Heartbeat │ │ - Transcript │ │ +│ └─────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Two-Phase Design + +### Phase 1: Signaling WebSocket (Control Plane) + +**Purpose**: Authentication, session control, heartbeats + +| Responsibility | Description | +|----------------|-------------| +| Authentication | Validate signature, establish session | +| Media Server Discovery | Returns media server URL in handshake response | +| Stream Control | Start/stop streaming commands | +| Heartbeat | Keep connection alive (msg_type 12/13) | +| Event Notifications | Participant join/leave, sharing start/stop | + +**URL Source**: From `server_urls` in webhook payload + +**Message Flow**: +``` +Client Signaling Server + │ │ + │──── Handshake Request (1) ────────>│ + │<─── Handshake Response (2) ────────│ <- Contains media_server.server_urls + │ │ + │──── Client Ready (7) ─────────────>│ <- After media handshake complete + │ │ + │<─── Keep Alive Request (12) ───────│ + │──── Keep Alive Response (13) ─────>│ + │ │ +``` + +### Phase 2: Media WebSocket (Data Plane) + +**Purpose**: Actual audio, video, transcript, chat, screen share data + +| Responsibility | Description | +|----------------|-------------| +| Media Configuration | Set audio/video parameters (codec, resolution, fps) | +| Media Streaming | Receive binary media data | +| Heartbeat | Keep connection alive (msg_type 12/13) | + +**URL Source**: From signaling handshake response (`media_server.server_urls.all`) + +**Message Flow**: +``` +Client Media Server + │ │ + │──── Media Handshake Request (3) ──>│ <- With media_params + │<─── Media Handshake Response (4) ──│ + │ │ + │<─── Audio Data (14) ───────────────│ + │<─── Video Data (15) ───────────────│ + │<─── Screen Share Data (16) ────────│ + │<─── Transcript Data (17) ──────────│ + │<─── Chat Data (18) ────────────────│ + │ │ + │<─── Keep Alive Request (12) ───────│ + │──── Keep Alive Response (13) ─────>│ + │ │ +``` + +## Why Two Connections? + +| Benefit | Explanation | +|---------|-------------| +| **Separation of Concerns** | Control logic doesn't interfere with media streaming | +| **Independent Scaling** | Signaling and media servers scale differently | +| **Fault Isolation** | Media reconnection doesn't require re-auth | +| **Split Mode Support** | Each media type can have its own connection | + +## Connection Modes + +### Split Mode (Recommended) + +Each media type gets its own dedicated WebSocket connection: + +``` +Signaling WS ─────┬───> Audio WS + ├───> Video WS + ├───> Transcript WS + └───> Screen Share WS +``` + +**Advantages**: +- Independent reconnection per media type +- Better reliability +- Fault isolation + +### Unified Mode + +One media WebSocket for all media types: + +``` +Signaling WS ─────> Media WS (all types) +``` + +**When to use**: +- Real-time audio+video muxing where sync matters +- Simpler implementation for small projects + +## Signature Generation + +Both signaling and media handshakes require HMAC-SHA256 signature: + +```javascript +// For meetings and webinars: use meeting_uuid +const message = `${clientId},${meetingUuid},${streamId}`; +// For Video SDK: use session_id +const message = `${clientId},${sessionId},${streamId}`; + +// Generic approach: use whichever ID is present +const idValue = payload.meeting_uuid || payload.session_id; +const message = `${clientId},${idValue},${streamId}`; +const signature = crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +``` + +> **Important**: Webinars use `meeting_uuid` (not `webinar_uuid`). Video SDK uses `session_id`. + +**Components**: +- `clientId`: OAuth Client ID (General App) or SDK Key (Video SDK App) +- `meetingUuid` / `sessionId`: From webhook payload (`meeting_uuid` for meetings/webinars, `session_id` for Video SDK) +- `streamId`: From webhook payload (`rtms_stream_id`) +- `clientSecret`: OAuth Client Secret (General App) or SDK Secret (Video SDK App) + +## Heartbeat Protocol + +**CRITICAL**: Both connections require heartbeat responses. + +When you receive `msg_type: 12` (Keep Alive Request): + +```javascript +// Immediately respond with msg_type: 13 +ws.send(JSON.stringify({ + msg_type: 13, + timestamp: receivedMessage.timestamp +})); +``` + +**Timeout**: +- Signaling: ~60 seconds without heartbeat response +- Media: ~65 seconds without heartbeat response + +**Failure to respond = connection closed!** + +## Reconnection + +RTMS does **NOT** auto-reconnect. You must implement: + +```javascript +ws.on('close', (code, reason) => { + console.log(`Connection closed: ${code} ${reason}`); + + // Implement exponential backoff + setTimeout(() => { + reconnect(); + }, retryDelay); + + retryDelay = Math.min(retryDelay * 2, 30000); +}); +``` + +**Timeouts**: +| Connection | Reconnection Window | +|------------|---------------------| +| Signaling | 60 seconds | +| Media | 65 seconds | + +## Server URL Geo-Routing + +Server URLs contain region codes: + +| Code | Location | +|------|----------| +| `sjc` | San Jose, California | +| `iad` | Washington DC | +| `sin` | Singapore | +| `fra` | Frankfurt, Germany | +| `syd` | Sydney, Australia | + +**Example**: `wss://rtms-sjc1.zoom.us/...` + +For production, route to workers in the same region as the Zoom server for lower latency. + +## Next Steps + +- **[Lifecycle Flow](lifecycle-flow.md)** - Complete webhook-to-streaming sequence +- **[SDK Quickstart](../examples/sdk-quickstart.md)** - SDK handles all this for you +- **[Manual WebSocket](../examples/manual-websocket.md)** - Full protocol implementation diff --git a/partner-built/zoom-plugin/skills/rtms/concepts/lifecycle-flow.md b/partner-built/zoom-plugin/skills/rtms/concepts/lifecycle-flow.md new file mode 100644 index 00000000..1cc1c006 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/concepts/lifecycle-flow.md @@ -0,0 +1,494 @@ +# RTMS Lifecycle Flow + +Complete flow from meeting/webinar/session start to media streaming. + +## High-Level Flow + +``` +┌─────────────────────────────┐ +│ Meeting/Webinar/Session │ +│ Starts │ +└────────────┬────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Zoom sends webhook event │ +│ meeting.rtms_started OR │ +│ webinar.rtms_started OR │ +│ session.rtms_started │ +└────────────┬────────────────┘ + │ + ▼ +┌──────────────────┐ +│ Your server │ +│ receives │ +│ webhook │ +│ │ +│ RESPOND 200 │ +│ IMMEDIATELY! │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Connect to │ +│ Signaling WS │ +│ │ +│ Send handshake │ +│ (msg_type: 1) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive │ +│ handshake resp │ +│ (msg_type: 2) │ +│ │ +│ Extract media │ +│ server URL │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Connect to │ +│ Media WS │ +│ │ +│ Send handshake │ +│ (msg_type: 3) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive media │ +│ handshake resp │ +│ (msg_type: 4) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Send Client │ +│ Ready to │ +│ Signaling │ +│ (msg_type: 7) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive media │ +│ data: │ +│ - Audio (14) │ +│ - Video (15) │ +│ - Share (16) │ +│ - Transcript(17)│ +│ - Chat (18) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Respond to │ +│ heartbeats │ +│ (12 -> 13) │ +└────────┬─────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Optional control-plane │ +│ actions during stream │ +│ - EVENT_SUBSCRIPTION │ +│ - VIDEO_SUBSCRIPTION_REQ │ +│ - STREAM_CLOSE_REQ │ +└────────────┬────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ meeting/webinar/session │ +│ .rtms_stopped │ +│ │ +│ Close sockets │ +│ Cleanup │ +└─────────────────────────────┘ +``` + +## Detailed Steps + +### Step 1: Receive Webhook + +When RTMS starts, Zoom sends a webhook. The event name and payload differ by product: + +**Meeting RTMS:** +```json +{ + "event": "meeting.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "meeting_id": "123456789", + "meeting_uuid": "AbC123...", + "host_id": "user123", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +**Webinar RTMS:** +```json +{ + "event": "webinar.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "meeting_id": "123456789", + "meeting_uuid": "AbC123...", + "host_id": "user123", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +> **Note**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`. + +**Video SDK RTMS:** +```json +{ + "event": "session.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "session_id": "SessionABC...", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +> **Note**: Video SDK payloads use `session_id` instead of `meeting_uuid`. + +### Product Differences + +| Aspect | Meetings | Webinars | Video SDK | +|--------|----------|----------|-----------| +| Webhook event | `meeting.rtms_started` | `webinar.rtms_started` | `session.rtms_started` | +| Payload ID field | `meeting_uuid` | `meeting_uuid` (same!) | `session_id` | +| App type | General App (OAuth) | General App (OAuth) | Video SDK App (SDK Key/Secret) | +| Participants | All participants | Panelists have full streams; attendees may not | All participants | +| Protocol after connect | Identical | Identical | Identical | + +**CRITICAL**: Respond with HTTP 200 **IMMEDIATELY** before any processing! + +```javascript +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; + +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + const { event, payload } = req.body; + if (RTMS_EVENTS.includes(event)) { + handleRTMSStarted(payload); + } +}); +``` + +**Why?** If you delay, Zoom retries the webhook. The retry creates a second connection, which kicks out your first connection. + +### Step 2: Connect to Signaling WebSocket + +```javascript +const signalingWs = new WebSocket(payload.server_urls); + +// Use meeting_uuid for meetings/webinars, session_id for Video SDK +const idValue = payload.meeting_uuid || payload.session_id; + +signalingWs.on('open', () => { + const signature = generateSignature( + CLIENT_ID, + idValue, + payload.rtms_stream_id, + CLIENT_SECRET + ); + + signalingWs.send(JSON.stringify({ + msg_type: 1, // Handshake request + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id: payload.rtms_stream_id, + signature: signature, + media_type: 9 // Audio(1) + Transcript(8) + })); +}); +``` + +### Step 3: Handle Signaling Response + +```javascript +signalingWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 2: // Handshake response + if (msg.status_code === 0) { + // Extract media server URL + const mediaUrl = msg.media_server.server_urls.all; + connectToMediaServer(mediaUrl); + } else { + console.error('Handshake failed:', msg.status_code); + } + break; + + case 12: // Keep alive request + signalingWs.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + break; + } +}); +``` + +### Step 4: Connect to Media WebSocket + +```javascript +function connectToMediaServer(mediaUrl) { + const mediaWs = new WebSocket(mediaUrl); + + mediaWs.on('open', () => { + mediaWs.send(JSON.stringify({ + msg_type: 3, // Media handshake request + protocol_version: 1, + meeting_uuid: idValue, // meeting_uuid or session_id + rtms_stream_id: streamId, + signature: signature, + media_type: 9, // Audio + Transcript + payload_encryption: false, + media_params: { + audio: { + content_type: 2, // RAW_AUDIO + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 // 20ms chunks + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch + } + } + })); + }); +} +``` + +### Step 5: Start Streaming + +After media handshake succeeds, tell signaling you're ready: + +```javascript +mediaWs.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 4 && msg.status_code === 0) { + // Media handshake success - tell signaling we're ready + signalingWs.send(JSON.stringify({ + msg_type: 7, // Client ready + rtms_stream_id: streamId + })); + } +}); +``` + +### Step 6: Receive Media Data + +```javascript +mediaWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 14: // Audio + const audioBuffer = Buffer.from(msg.content, 'base64'); + processAudio(audioBuffer, msg.user_name, msg.timestamp); + break; + + case 15: // Video + const videoBuffer = Buffer.from(msg.content, 'base64'); + processVideo(videoBuffer, msg.user_name, msg.timestamp); + break; + + case 16: // Screen share + const shareBuffer = Buffer.from(msg.content, 'base64'); + processScreenShare(shareBuffer, msg.user_name, msg.timestamp); + break; + + case 17: // Transcript + console.log(`${msg.user_name}: ${msg.content}`); + break; + + case 18: // Chat + console.log(`[Chat] ${msg.user_name}: ${msg.content}`); + break; + + case 12: // Keep alive + mediaWs.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + break; + } +}); +``` + +### Step 6A: Track Available Participant Video Streams + +When using the new single-individual-video mode, the signaling socket tells you whose camera is currently available. + +```javascript +const activeVideoUsers = new Set(); + +function handleEventUpdate(msg) { + const eventType = msg.event?.event_type; + const participants = msg.event?.participants || []; + + if (eventType === 8) { // PARTICIPANT_VIDEO_ON + for (const participant of participants) activeVideoUsers.add(participant.user_id); + } + + if (eventType === 9) { // PARTICIPANT_VIDEO_OFF + for (const participant of participants) activeVideoUsers.delete(participant.user_id); + } +} +``` + +Use these events as the control-plane signal for which participant video streams are currently subscribable. + +### Step 6B: Select One Participant Video Stream + +```javascript +function subscribeToParticipantVideo(streamId, userId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: userId, + subscribe: true, + timestamp: Date.now() + })); +} +``` + +Important constraint: + +- only one participant stream can be active at a time +- the newest successful subscription replaces the previous selection + +### Step 7: Handle Session End + +```javascript +const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped']; + +// Via webhook +app.post('/webhook', (req, res) => { + res.status(200).send(); + + const { event, payload } = req.body; + + if (RTMS_STOP_EVENTS.includes(event)) { + const streamId = payload.rtms_stream_id; + + // Close connections + signalingConnections.get(streamId)?.close(); + mediaConnections.get(streamId)?.close(); + + // Cleanup + signalingConnections.delete(streamId); + mediaConnections.delete(streamId); + } +}); + +// Also handle WebSocket close events +signalingWs.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason); + // Implement reconnection if needed +}); +``` + +### Optional: Client-Initiated Graceful Close + +The backend can now ask RTMS to terminate the stream cleanly: + +```javascript +function closeStream(streamId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId + })); +} +``` + +Expect a `STREAM_CLOSE_RESP` followed by normal socket teardown. + +## Session Tracking + +**CRITICAL**: Track active sessions to prevent duplicate connections! + +```javascript +const activeSessions = new Map(); + +function handleRTMSStarted(payload) { + const streamId = payload.rtms_stream_id; + + // Check for existing connection + if (activeSessions.has(streamId)) { + console.log('Already connected to this stream, ignoring'); + return; + } + + // Mark as active (meeting_uuid for meetings/webinars, session_id for Video SDK) + activeSessions.set(streamId, { + startTime: Date.now(), + idValue: payload.meeting_uuid || payload.session_id + }); + + // Connect + connectToRTMS(payload); +} + +function handleRTMSStopped(payload) { + const streamId = payload.rtms_stream_id; + activeSessions.delete(streamId); + // ... cleanup +} +``` + +## Error Handling + +```javascript +// SDK state management (from Arlo sample) +try { + client.join(payload); +} catch (error) { + if (error.message?.includes('Invalid status')) { + console.warn('SDK in invalid state, waiting to retry...'); + + setTimeout(() => { + handleRTMSStarted(payload); + }, 2000); + } +} +``` + +## Next Steps + +- **[SDK Quickstart](../examples/sdk-quickstart.md)** - SDK handles all this automatically +- **[Manual WebSocket](../examples/manual-websocket.md)** - Full implementation code +- **[Common Issues](../troubleshooting/common-issues.md)** - Debugging connection problems diff --git a/partner-built/zoom-plugin/skills/rtms/examples/ai-integration.md b/partner-built/zoom-plugin/skills/rtms/examples/ai-integration.md new file mode 100644 index 00000000..f85f7831 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/examples/ai-integration.md @@ -0,0 +1,436 @@ +# AI Integration Patterns + +Patterns for integrating RTMS with AI services for transcription, analysis, and meeting assistants. These examples work with meetings, webinars, and Video SDK sessions. + +## Audio Transcription with External Services + +### Deepgram Integration + +```javascript +import rtms from "@zoom/rtms"; +import { createClient } from "@deepgram/sdk"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const deepgram = createClient(process.env.DEEPGRAM_API_KEY); + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + // Configure for Deepgram-compatible audio + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1, // Mono + dataOpt: 1 // Mixed stream + }); + + // Create live transcription connection + const connection = deepgram.listen.live({ + model: "nova-2", + language: "en", + smart_format: true, + punctuate: true, + }); + + connection.on("Results", (data) => { + const transcript = data.channel.alternatives[0].transcript; + if (transcript) { + console.log(`[Deepgram]: ${transcript}`); + } + }); + + client.onAudioData((buffer, timestamp, metadata) => { + // Send audio to Deepgram + connection.send(buffer); + }); + + client.onLeave(() => { + connection.finish(); + }); + + client.join(payload); +}); +``` + +### AssemblyAI Integration + +```javascript +import rtms from "@zoom/rtms"; +import { AssemblyAI } from "assemblyai"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const aai = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY }); + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1 // Mono + }); + + const transcriber = aai.realtime.createService({ + sampleRate: 16000, + }); + + transcriber.connect(); + + transcriber.on("transcript", (transcript) => { + if (transcript.text) { + console.log(`[AssemblyAI]: ${transcript.text}`); + } + }); + + client.onAudioData((buffer, timestamp, metadata) => { + transcriber.sendAudio(buffer); + }); + + client.onLeave(() => { + transcriber.close(); + }); + + client.join(payload); +}); +``` + +### Whisper (Local) Integration + +```javascript +import rtms from "@zoom/rtms"; +import { Whisper } from "whisper-node"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const whisper = new Whisper("base.en"); + +let audioBuffer = Buffer.alloc(0); +const BUFFER_SIZE = 16000 * 10; // 10 seconds at 16kHz + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1 // Mono + }); + + client.onAudioData(async (buffer, timestamp, metadata) => { + // Accumulate audio + audioBuffer = Buffer.concat([audioBuffer, buffer]); + + // Transcribe when buffer is full + if (audioBuffer.length >= BUFFER_SIZE) { + const transcript = await whisper.transcribe(audioBuffer); + console.log(`[Whisper]: ${transcript}`); + audioBuffer = Buffer.alloc(0); + } + }); + + client.join(payload); +}); +``` + +## Meeting Summarization + +### OpenAI/GPT Integration + +```javascript +import rtms from "@zoom/rtms"; +import OpenAI from "openai"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const transcripts = []; +let summaryInterval; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + transcripts.push({ + speaker: metadata.userName, + text: text, + time: new Date(timestamp) + }); + }); + + // Generate summary every 5 minutes + summaryInterval = setInterval(async () => { + if (transcripts.length === 0) return; + + const fullTranscript = transcripts + .map(t => `${t.speaker}: ${t.text}`) + .join('\n'); + + const summary = await openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { + role: "system", + content: "Summarize this meeting transcript. Include key points, decisions, and action items." + }, + { + role: "user", + content: fullTranscript + } + ] + }); + + console.log("Meeting Summary:", summary.choices[0].message.content); + }, 5 * 60 * 1000); + + client.onLeave(async () => { + clearInterval(summaryInterval); + + // Generate final summary + const fullTranscript = transcripts + .map(t => `${t.speaker}: ${t.text}`) + .join('\n'); + + const summary = await openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { + role: "system", + content: `Create a comprehensive meeting summary with: +- Key topics discussed +- Decisions made +- Action items with owners +- Follow-up items` + }, + { + role: "user", + content: fullTranscript + } + ] + }); + + console.log("Final Summary:", summary.choices[0].message.content); + }); + + client.join(payload); +}); +``` + +## Real-Time Sentiment Analysis + +```javascript +import rtms from "@zoom/rtms"; + +async function analyzeSentiment(text) { + // Use any sentiment API (OpenAI, HuggingFace, etc.) + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [{ + role: 'user', + content: `Analyze sentiment (positive/neutral/negative): "${text}"` + }] + }) + }); + + const data = await response.json(); + return data.choices[0].message.content; +} + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + let recentTranscripts = []; + + client.onTranscriptData(async (buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + recentTranscripts.push(text); + + // Analyze every 10 segments + if (recentTranscripts.length >= 10) { + const combinedText = recentTranscripts.join(' '); + const sentiment = await analyzeSentiment(combinedText); + console.log(`Sentiment: ${sentiment}`); + recentTranscripts = []; + } + }); + + client.join(payload); +}); +``` + +## Audio Recording with Gap Filling + +For continuous playback, fill audio gaps with silence: + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const SAMPLE_RATE = 16000; +const BYTES_PER_SAMPLE = 2; // 16-bit +const MS_PER_FRAME = 20; +const BYTES_PER_FRAME = SAMPLE_RATE * BYTES_PER_SAMPLE * MS_PER_FRAME / 1000; + +function generateSilentFrame(durationMs) { + const samples = SAMPLE_RATE * durationMs / 1000; + return Buffer.alloc(samples * BYTES_PER_SAMPLE); +} + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + const streamId = payload.rtms_stream_id; + + const audioStream = fs.createWriteStream(`recordings/${streamId}.pcm`); + let lastTimestamp = null; + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1, // Mono + dataOpt: 1, // Mixed stream + duration: 20 // 20ms chunks + }); + + client.onAudioData((buffer, timestamp, metadata) => { + if (lastTimestamp !== null) { + const gap = timestamp - lastTimestamp; + + // Fill gaps >= 500ms with silence + if (gap >= 500) { + const silentFrames = Math.floor(gap / MS_PER_FRAME); + console.log(`Gap detected: ${gap}ms, filling ${silentFrames} frames`); + + for (let i = 0; i < silentFrames; i++) { + audioStream.write(generateSilentFrame(MS_PER_FRAME)); + } + } + } + + lastTimestamp = timestamp; + audioStream.write(buffer); + }); + + client.onLeave(() => { + audioStream.end(); + console.log(`Recording saved: recordings/${streamId}.pcm`); + }); + + client.join(payload); +}); +``` + +## Multi-Format Transcript Output + +Generate VTT, SRT, and TXT simultaneously: + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +function formatVttTimestamp(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + const h = Math.floor(m / 60); + const msec = ms % 1000; + return `${String(h).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}.${String(msec).padStart(3, '0')}`; +} + +function formatSrtTimestamp(ms) { + return formatVttTimestamp(ms).replace('.', ','); +} + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + const streamId = payload.rtms_stream_id; + + const baseDir = `recordings/${streamId}`; + fs.mkdirSync(baseDir, { recursive: true }); + + fs.writeFileSync(`${baseDir}/transcript.vtt`, 'WEBVTT\n\n'); + let srtIndex = 1; + let startTimestamp = null; + + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + const userName = metadata.userName; + + if (startTimestamp === null) { + startTimestamp = timestamp; + } + + const relative = timestamp - startTimestamp; + const endTime = relative + 2000; // 2 second duration + + // VTT format + const vttLine = `${formatVttTimestamp(relative)} --> ${formatVttTimestamp(endTime)}\n${userName}: ${text}\n\n`; + fs.appendFileSync(`${baseDir}/transcript.vtt`, vttLine); + + // SRT format + const srtLine = `${srtIndex++}\n${formatSrtTimestamp(relative)} --> ${formatSrtTimestamp(endTime)}\n${userName}: ${text}\n\n`; + fs.appendFileSync(`${baseDir}/transcript.srt`, srtLine); + + // Plain text + const txtLine = `[${new Date(timestamp).toISOString()}] ${userName}: ${text}\n`; + fs.appendFileSync(`${baseDir}/transcript.txt`, txtLine); + }); + + client.join(payload); +}); +``` + +## Environment Variables + +```bash +# Zoom RTMS +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret + +# AI Services +OPENAI_API_KEY=sk-... +DEEPGRAM_API_KEY=... +ASSEMBLYAI_API_KEY=... + +# OpenRouter (free models) +OPENROUTER_API_KEY=sk-or-... +``` + +## Free AI Model Considerations + +When using free models (Gemma, Qwen, DeepSeek via OpenRouter): + +| Limitation | Impact | Solution | +|------------|--------|----------| +| No image support | Can't analyze screen shares | Use paid model or skip image analysis | +| Context limits | Long transcripts may fail | Chunk transcripts, summarize incrementally | +| Rate limiting | May get 429 errors | Implement retry with backoff, stagger requests | + +**Recommended for production**: OpenRouter with `google/gemini-2.5-pro` - supports vision + XML tagging. + +## Next Steps + +- **[SDK Quickstart](sdk-quickstart.md)** - Basic RTMS setup +- **[Manual WebSocket](manual-websocket.md)** - Protocol details +- **[Media Types](../references/media-types.md)** - Audio/video configuration diff --git a/partner-built/zoom-plugin/skills/rtms/examples/manual-websocket.md b/partner-built/zoom-plugin/skills/rtms/examples/manual-websocket.md new file mode 100644 index 00000000..e105e70a --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/examples/manual-websocket.md @@ -0,0 +1,580 @@ +# Manual WebSocket Implementation + +Full RTMS protocol implementation without the SDK. Use this for: +- Languages without SDK support +- Custom protocol requirements +- Learning the underlying protocol + +## Overview + +RTMS requires two WebSocket connections: +1. **Signaling WebSocket** - Control plane (handshake, heartbeat, start/stop) +2. **Media WebSocket** - Data plane (audio, video, transcript, chat, share) + +## Complete Implementation + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); +const express = require('express'); + +const app = express(); +app.use(express.json()); + +// Configuration +const CLIENT_ID = process.env.ZOOM_CLIENT_ID; +const CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET; +const SECRET_TOKEN = process.env.ZOOM_SECRET_TOKEN; + +// Active connections +const signalingConnections = new Map(); +const mediaConnections = new Map(); +const activeSessions = new Map(); +const activeVideoUsers = new Map(); + +// ============================================ +// SIGNATURE GENERATION +// Uses meeting_uuid for meetings/webinars, session_id for Video SDK +// ============================================ + +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +} + +// ============================================ +// WEBHOOK HANDLER +// ============================================ + +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; +const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped']; + +app.post('/webhook', (req, res) => { + // CRITICAL: Respond 200 IMMEDIATELY before any processing! + res.status(200).send(); + + const { event, payload } = req.body; + + // Handle URL validation challenge + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hash + }); + } + + // Handle RTMS events (meetings, webinars, and Video SDK) + if (RTMS_EVENTS.includes(event)) { + handleRTMSStarted(payload.object); + } else if (RTMS_STOP_EVENTS.includes(event)) { + handleRTMSStopped(payload.object); + } +}); + +// ============================================ +// RTMS START HANDLER +// ============================================ + +function handleRTMSStarted(payload) { + const { rtms_stream_id, server_urls } = payload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = payload.meeting_uuid || payload.session_id; + + // Prevent duplicate connections + if (activeSessions.has(rtms_stream_id)) { + console.log('Already connected to this stream, ignoring'); + return; + } + + activeSessions.set(rtms_stream_id, { + idValue: idValue, + startTime: Date.now() + }); + + connectToSignaling(idValue, rtms_stream_id, server_urls); +} + +// ============================================ +// SIGNALING WEBSOCKET +// ============================================ + +function connectToSignaling(idValue, streamId, serverUrl) { + console.log('Connecting to signaling:', serverUrl); + + const signature = generateSignature(CLIENT_ID, idValue, streamId, CLIENT_SECRET); + const ws = new WebSocket(serverUrl); + + signalingConnections.set(streamId, ws); + + ws.on('open', () => { + console.log('Signaling connected, sending handshake'); + + ws.send(JSON.stringify({ + msg_type: 1, // SIGNALING_HAND_SHAKE_REQ + protocol_version: 1, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + rtms_stream_id: streamId, + sequence: Math.floor(Math.random() * 1000000), + signature: signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data.toString()); + handleSignalingMessage(msg, idValue, streamId); + }); + + ws.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason.toString()); + signalingConnections.delete(streamId); + // Implement reconnection logic if needed + }); + + ws.on('error', (error) => { + console.error('Signaling error:', error); + }); +} + +function handleSignalingMessage(msg, idValue, streamId) { + switch (msg.msg_type) { + case 2: // SIGNALING_HAND_SHAKE_RESP + if (msg.status_code === 0) { + console.log('Signaling handshake success'); + + // Extract media server URL and connect + const mediaUrl = msg.media_server.server_urls.all; + connectToMedia(idValue, streamId, mediaUrl); + } else { + console.error('Signaling handshake failed:', msg.status_code); + } + break; + + case 6: // EVENT_UPDATE + handleEventUpdate(msg, streamId); + break; + + case 8: // STREAM_STATE_UPDATE + console.log('Stream state:', msg.state); + break; + + case 9: // SESSION_STATE_UPDATE + console.log('Session state:', msg.state); + break; + + case 12: // KEEP_ALIVE_REQ + const signalingWs = signalingConnections.get(streamId); + if (signalingWs) { + signalingWs.send(JSON.stringify({ + msg_type: 13, // KEEP_ALIVE_RESP + timestamp: msg.timestamp + })); + } + break; + } +} + +function handleEventUpdate(msg, streamId) { + const eventType = msg.event?.event_type ?? msg.event_type; + const participants = msg.event?.participants ?? []; + + switch (eventType) { + case 2: // ACTIVE_SPEAKER_CHANGE + console.log('Active speaker:', msg.user_name); + break; + case 3: // PARTICIPANT_JOIN + console.log('Participant joined:', msg.user_name); + break; + case 4: // PARTICIPANT_LEAVE + console.log('Participant left:', msg.user_name); + break; + case 5: // SHARING_START + console.log('Sharing started by:', msg.user_name); + break; + case 6: // SHARING_STOP + console.log('Sharing stopped'); + break; + case 8: // PARTICIPANT_VIDEO_ON + for (const participant of participants) { + const set = activeVideoUsers.get(streamId) || new Set(); + set.add(participant.user_id); + activeVideoUsers.set(streamId, set); + } + break; + case 9: // PARTICIPANT_VIDEO_OFF + for (const participant of participants) { + activeVideoUsers.get(streamId)?.delete(participant.user_id); + } + break; + } +} + +// ============================================ +// MEDIA WEBSOCKET +// ============================================ + +function connectToMedia(idValue, streamId, mediaUrl) { + console.log('Connecting to media:', mediaUrl); + + const signature = generateSignature(CLIENT_ID, idValue, streamId, CLIENT_SECRET); + const ws = new WebSocket(mediaUrl); + + mediaConnections.set(streamId, ws); + + ws.on('open', () => { + console.log('Media connected, sending handshake'); + + ws.send(JSON.stringify({ + msg_type: 3, // DATA_HAND_SHAKE_REQ + protocol_version: 1, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + rtms_stream_id: streamId, + signature: signature, + media_type: 9, // AUDIO(1) | TRANSCRIPT(8) + payload_encryption: false, + media_params: { + audio: { + content_type: 2, // RAW_AUDIO + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 // 20ms chunks + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch + } + } + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data.toString()); + handleMediaMessage(msg, streamId); + }); + + ws.on('close', (code, reason) => { + console.log('Media closed:', code, reason.toString()); + mediaConnections.delete(streamId); + }); + + ws.on('error', (error) => { + console.error('Media error:', error); + }); +} + +function handleMediaMessage(msg, streamId) { + switch (msg.msg_type) { + case 4: // DATA_HAND_SHAKE_RESP + if (msg.status_code === 0) { + console.log('Media handshake success, sending client ready'); + + // Tell signaling we're ready to receive + const signalingWs = signalingConnections.get(streamId); + if (signalingWs) { + signalingWs.send(JSON.stringify({ + msg_type: 7, // CLIENT_READY_ACK + rtms_stream_id: streamId + })); + } + } else { + console.error('Media handshake failed:', msg.status_code); + } + break; + + case 12: // KEEP_ALIVE_REQ + const mediaWs = mediaConnections.get(streamId); + if (mediaWs) { + mediaWs.send(JSON.stringify({ + msg_type: 13, // KEEP_ALIVE_RESP + timestamp: msg.timestamp + })); + } + break; + + case 14: // MEDIA_DATA_AUDIO + handleAudioData(msg); + break; + + case 15: // MEDIA_DATA_VIDEO + handleVideoData(msg); + break; + + case 16: // MEDIA_DATA_SHARE + handleShareData(msg); + break; + + case 17: // MEDIA_DATA_TRANSCRIPT + handleTranscriptData(msg); + break; + + case 18: // MEDIA_DATA_CHAT + handleChatData(msg); + break; + } +} + +// ============================================ +// MEDIA DATA HANDLERS +// ============================================ + +function handleAudioData(msg) { + const audioBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Audio: ${audioBuffer.length} bytes from ${msg.user_name || 'mixed'}`); + + // Process audio: + // - Send to transcription service + // - Save to file + // - Stream to output +} + +function handleVideoData(msg) { + const videoBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Video: ${videoBuffer.length} bytes from ${msg.user_name}`); + + // Process video: + // - Decode H.264/JPG + // - Save frames + // - AI analysis +} + +function handleShareData(msg) { + const shareBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Share: ${shareBuffer.length} bytes from ${msg.user_name}`); +} + +function handleTranscriptData(msg) { + console.log(`[${msg.user_name}]: ${msg.content}`); + + // Save transcript, process with AI, etc. +} + +function handleChatData(msg) { + console.log(`[Chat] ${msg.user_name}: ${msg.content}`); +} + +// ============================================ +// RTMS STOP HANDLER +// ============================================ + +function handleRTMSStopped(payload) { + const streamId = payload.rtms_stream_id; + + console.log('RTMS stopped:', streamId); + + // Close connections + const signalingWs = signalingConnections.get(streamId); + const mediaWs = mediaConnections.get(streamId); + + if (signalingWs) signalingWs.close(); + if (mediaWs) mediaWs.close(); + + // Cleanup + signalingConnections.delete(streamId); + mediaConnections.delete(streamId); + activeSessions.delete(streamId); +} + +// ============================================ +// START SERVER +// ============================================ + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`RTMS server running on port ${PORT}`); +}); +``` + +## Message Type Reference + +### Signaling Messages + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 1 | SIGNALING_HAND_SHAKE_REQ | Client -> Server | Initial handshake | +| 2 | SIGNALING_HAND_SHAKE_RESP | Server -> Client | Handshake response with media URL | +| 5 | EVENT_SUBSCRIPTION | Client -> Server | Subscribe to events | +| 6 | EVENT_UPDATE | Server -> Client | Event notification | +| 7 | CLIENT_READY_ACK | Client -> Server | Ready to receive media | +| 8 | STREAM_STATE_UPDATE | Server -> Client | Stream state changed | +| 9 | SESSION_STATE_UPDATE | Server -> Client | Session state changed | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | + +### Media Messages + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 3 | DATA_HAND_SHAKE_REQ | Client -> Server | Media handshake with params | +| 4 | DATA_HAND_SHAKE_RESP | Server -> Client | Media handshake response | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | +| 14 | MEDIA_DATA_AUDIO | Server -> Client | Audio data | +| 15 | MEDIA_DATA_VIDEO | Server -> Client | Video data | +| 16 | MEDIA_DATA_SHARE | Server -> Client | Screen share data | +| 17 | MEDIA_DATA_TRANSCRIPT | Server -> Client | Transcript data | +| 18 | MEDIA_DATA_CHAT | Server -> Client | Chat message | + +## Media Parameters + +### Audio Parameters + +```javascript +{ + content_type: 2, // 1=RTP, 2=RAW_AUDIO + sample_rate: 1, // 0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz + channel: 1, // 1=Mono, 2=Stereo (OPUS only) + codec: 1, // 1=L16, 2=G.711, 3=G.722, 4=OPUS + data_opt: 1, // 1=Mixed, 2=Multi-streams + send_rate: 20 // Chunk size in ms (multiple of 20) +} + +function subscribeToParticipantVideo(streamId, userId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: userId, + subscribe: true, + timestamp: Date.now() + })); +} + +function closeStream(streamId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId + })); +} +``` + +## March 2026 Notes + +- The new `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` events tell you which participants currently have subscribable camera streams. +- To receive one participant camera feed, use `VIDEO_SINGLE_INDIVIDUAL_STREAM` in the video media handshake and then send `VIDEO_SUBSCRIPTION_REQ`. +- RTMS currently supports only **one** individual participant video stream at a time. A new subscription replaces the previous one. +- `STREAM_CLOSE_REQ` / `STREAM_CLOSE_RESP` let the backend terminate a stream cleanly. +- Exact numeric values: + - `PARTICIPANT_VIDEO_ON = 8` + - `PARTICIPANT_VIDEO_OFF = 9` + - `STREAM_CLOSE_REQ = 21` + - `STREAM_CLOSE_RESP = 22` + - `VIDEO_SUBSCRIPTION_REQ = 28` + - `VIDEO_SUBSCRIPTION_RESP = 29` + +### Video Parameters + +```javascript +{ + content_type: 3, // 3=RAW_VIDEO + codec: 7, // 5=JPG, 6=PNG, 7=H.264 + resolution: 2, // 1=SD, 2=HD, 3=FHD, 4=QHD + fps: 25, // 1-30 (JPG/PNG max 5) + data_opt: 3 // 3=Single active speaker +} +``` + +### Screen Share Parameters + +```javascript +{ + content_type: 3, // 3=RAW_VIDEO + codec: 5, // 5=JPG, 6=PNG, 7=H.264 + resolution: 3, // 1=SD, 2=HD, 3=FHD, 4=QHD + fps: 1 // 1-30 (JPG/PNG max 1) +} +``` + +### Transcript Parameters + +```javascript +{ + content_type: 5, // 5=TEXT + src_language: 9, // 9=English + enable_lid: false // Fixed language, no auto-switch +} +``` + +## Status Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | STATUS_OK | Success | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Duplicate signaling connection | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Duplicate media connection | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +See [Data Types](../references/data-types.md) for complete list. + +## Error Handling + +```javascript +// Implement exponential backoff for reconnection +let retryDelay = 1000; + +ws.on('close', (code, reason) => { + console.log('Connection closed:', code, reason); + + // Don't reconnect if intentionally closed + if (code === 1000) return; + + setTimeout(() => { + reconnect(); + }, retryDelay); + + retryDelay = Math.min(retryDelay * 2, 30000); +}); + +ws.on('error', (error) => { + console.error('WebSocket error:', error); + // Connection will close, triggering reconnection +}); +``` + +## Gap-Filled Audio Recording + +Fill gaps with silence for continuous playback: + +```javascript +function handleAudioData(msg, streamId) { + const now = msg.timestamp; + const last = lastTimestamps.get(streamId) || now; + const gap = now - last; + + // Fill gaps >= 500ms with silence + if (gap >= 500) { + const silentFrames = Math.floor(gap / 20); + console.log(`Filling ${silentFrames} silent frames`); + + for (let i = 0; i < silentFrames; i++) { + const silentFrame = Buffer.alloc(640); // 20ms @ 16kHz mono + writeToFile(silentFrame); + } + } + + lastTimestamps.set(streamId, now); + + const audioBuffer = Buffer.from(msg.content, 'base64'); + writeToFile(audioBuffer); +} +``` + +## Next Steps + +- **[SDK Quickstart](sdk-quickstart.md)** - SDK handles all this complexity +- **[AI Integration](ai-integration.md)** - Transcription and analysis +- **[Data Types](../references/data-types.md)** - All enums and constants diff --git a/partner-built/zoom-plugin/skills/rtms/examples/rtms-bot.md b/partner-built/zoom-plugin/skills/rtms/examples/rtms-bot.md new file mode 100644 index 00000000..2b7fcc14 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/examples/rtms-bot.md @@ -0,0 +1,696 @@ +# RTMS Bot (Real-Time Media Streams) + +Build resilient RTMS bots that access meeting audio, video, transcription, screen share, and chat without joining as a visible participant. + +## Overview + +RTMS bots are invisible read-only services that subscribe to meeting media streams via WebSockets. They do NOT appear in the participant list. + +**Use this approach when:** +- You only need to observe/transcribe (no interaction needed) +- You want invisible operation +- You're processing external meetings (with permission) +- You want minimal resource usage + +**Alternative:** See [Meeting SDK Bot (Linux)](../../meeting-sdk/linux/meeting-sdk-bot.md) for visible participant bots with full meeting control. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ RTMS BOT FLOW │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. Trigger RTMS: REST API or In-Meeting Start │ +│ └── POST /meetings/{meetingId}/rtms │ +│ └── Or: Start RTMS manually from Zoom client │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 2. Wait for Webhook: meeting.rtms_started │ +│ └── Zoom sends signaling + media WebSocket URLs │ +│ └── No webhook = RTMS unavailable (no polling fallback) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 3. Connect: Signaling WebSocket (Handshake with HMAC) │ +│ └── Generate HMAC-SHA256 signature │ +│ └── Send handshake message │ +│ └── Receive session confirmation │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 4. Connect: Media WebSocket (Subscribe to Streams) │ +│ └── Subscribe to: audio, video, transcription, share, chat │ +│ └── Send keep-alive pings │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 5. Process Media Data │ +│ └── Audio: Opus/PCM streams per speaker │ +│ └── Video: H.264 encoded frames │ +│ └── Transcription: Real-time text with speaker labels │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 6. Mid-Stream: Connection Monitoring │ +│ └── Detect WebSocket close → Exponential backoff retry │ +│ └── Stop after N reconnection attempts │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Skills Required + +| Skill | Purpose | +|-------|---------| +| **zoom-rest-api** | Trigger RTMS start (optional - can also start manually) | +| **rtms** | WebSocket connection, media processing | +| **webhooks** | Receive `meeting.rtms_started` event | + +## Prerequisites + +- Zoom app with RTMS features enabled +- Webhook endpoint (HTTPS, publicly accessible) +- Event subscriptions: `meeting.rtms_started`, `meeting.rtms_stopped` +- Scopes: `meeting:read:admin`, `meeting:write:admin` (if triggering via API) +- RTMS SDK or native WebSocket implementation + +## Configuration + +### Retry Parameters (Customizable) + +```javascript +// config.js or environment variables +const rtmsConfig = { + // WebSocket connection (initial) + connection_timeout_ms: 10000, // Handshake timeout (default: 10s) + connection_max_attempts: 5, // Max connection attempts (default: 5) + connection_retry_delay_ms: 5000, // Constant retry: 5s (default: 5s) + + // Mid-stream reconnection (network failures) + reconnect_max_attempts: 3, // Max reconnection attempts (default: 3) + reconnect_base_delay_ms: 2000, // Initial delay: 2s (default: 2s) + // Exponential backoff: 2s, 4s, 8s... + + // Keep-alive ping + keepalive_interval_ms: 5000, // Send ping every 5s (default: 5s, min: 3s) + keepalive_timeout_ms: 15000, // Expect pong within 15s (default: 15s) + + // Webhook wait timeout + webhook_wait_timeout_ms: 300000 // Wait 5min for webhook (default: 5min) +}; + +// Load from environment variables (recommended for production) +function loadConfig() { + return { + connection_timeout_ms: + parseInt(process.env.RTMS_CONNECTION_TIMEOUT_MS) || 10000, + connection_max_attempts: + parseInt(process.env.RTMS_CONNECTION_MAX_ATTEMPTS) || 5, + connection_retry_delay_ms: + parseInt(process.env.RTMS_CONNECTION_RETRY_DELAY_MS) || 5000, + reconnect_max_attempts: + parseInt(process.env.RTMS_RECONNECT_MAX_ATTEMPTS) || 3, + reconnect_base_delay_ms: + parseInt(process.env.RTMS_RECONNECT_BASE_DELAY_MS) || 2000, + keepalive_interval_ms: + Math.max(parseInt(process.env.RTMS_KEEPALIVE_INTERVAL_MS) || 5000, 3000), + keepalive_timeout_ms: + parseInt(process.env.RTMS_KEEPALIVE_TIMEOUT_MS) || 15000, + webhook_wait_timeout_ms: + parseInt(process.env.RTMS_WEBHOOK_WAIT_TIMEOUT_MS) || 300000 + }; +} +``` + +### Customization Guide + +| Parameter | Default | When to Increase | When to Decrease | +|-----------|---------|------------------|------------------| +| `connection_max_attempts` | 5 | Slow/congested networks | Fast failure detection needed | +| `connection_retry_delay_ms` | 5000 (5s) | High network latency | Local network, low latency | +| `reconnect_max_attempts` | 3 | Critical meetings, unstable network | Cost-sensitive, batch processing | +| `reconnect_base_delay_ms` | 2000 (2s) | International connections | Local network | +| `keepalive_interval_ms` | 5000 (5s) | Aggressive connection monitoring | Reduce bandwidth overhead | +| `webhook_wait_timeout_ms` | 300000 (5min) | Meetings may start late | Fast failure detection | + +**Recommended Ranges:** +- Connection attempts: 3-10 +- Connection retry delay: 2s-15s +- Reconnect attempts: 2-5 +- Reconnect base delay: 1s-5s +- Keep-alive interval: 3s-30s (min: 3s per Zoom docs) + +**Examples:** + +```bash +# High-priority production bot (aggressive) +export RTMS_CONNECTION_MAX_ATTEMPTS=10 +export RTMS_CONNECTION_RETRY_DELAY_MS=3000 # 3s +export RTMS_RECONNECT_MAX_ATTEMPTS=5 +export RTMS_RECONNECT_BASE_DELAY_MS=1000 # 1s +export RTMS_KEEPALIVE_INTERVAL_MS=3000 # 3s (minimum) + +# Cost-sensitive batch processing (conservative) +export RTMS_CONNECTION_MAX_ATTEMPTS=3 +export RTMS_CONNECTION_RETRY_DELAY_MS=10000 # 10s +export RTMS_RECONNECT_MAX_ATTEMPTS=2 +export RTMS_RECONNECT_BASE_DELAY_MS=5000 # 5s +export RTMS_KEEPALIVE_INTERVAL_MS=15000 # 15s + +# Development/testing (fail fast) +export RTMS_CONNECTION_MAX_ATTEMPTS=2 +export RTMS_CONNECTION_RETRY_DELAY_MS=2000 # 2s +export RTMS_RECONNECT_MAX_ATTEMPTS=1 +export RTMS_RECONNECT_BASE_DELAY_MS=1000 # 1s +export RTMS_WEBHOOK_WAIT_TIMEOUT_MS=60000 # 1min +``` + +## Step 1: Trigger RTMS (Optional - REST API) + +You can start RTMS programmatically or manually from the Zoom client. + +### Option A: REST API Trigger + +```bash +# Start RTMS for a meeting +curl -X POST "https://api.zoom.us/v2/meetings/{meetingId}/rtms" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "meeting" + }' +``` + +**Response:** +```json +{ + "rtms_id": "abc123def456", + "status": "starting" +} +``` + +### Option B: Manual Start (In-Meeting) + +Host clicks **Apps** → Your RTMS App → **Start RTMS** + +**Both trigger the same webhook** → `meeting.rtms_started` + +## Step 2: Wait for Webhook (Required) + +**CRITICAL:** RTMS requires a webhook. There is NO polling alternative. If webhook doesn't arrive, RTMS is unavailable. + +### Webhook Handler + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const app = express(); + +const config = loadConfig(); +const pendingConnections = new Map(); // Track webhook waiters + +app.post('/webhook', express.json(), async (req, res) => { + // 1. Verify webhook signature + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!verifyWebhookSignature(req.body, signature, timestamp)) { + return res.status(403).send('Invalid signature'); + } + + // 2. Respond immediately (Zoom expects 200 within 3s) + res.status(200).send(); + + // 3. Process webhook asynchronously + const event = req.body; + + if (event.event === 'meeting.rtms_started') { + console.log('[WEBHOOK] RTMS started for meeting:', event.payload.object.uuid); + + const rtmsInfo = { + meetingUuid: event.payload.object.uuid, + signalingUrl: event.payload.object.signaling_url, + mediaUrl: event.payload.object.media_url, + sessionKey: event.payload.object.session_key + }; + + // Notify waiting connection + const waiter = pendingConnections.get(rtmsInfo.meetingUuid); + if (waiter) { + waiter.resolve(rtmsInfo); + pendingConnections.delete(rtmsInfo.meetingUuid); + } else { + // No waiter - proactive start + connectToRTMS(rtmsInfo); + } + } +}); + +function verifyWebhookSignature(body, signature, timestamp) { + const message = `v0:${timestamp}:${JSON.stringify(body)}`; + const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET_TOKEN); + const computed = 'v0=' + hmac.update(message).digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(computed) + ); +} +``` + +### Wait for Webhook (with Timeout) + +```javascript +async function waitForRTMSWebhook(meetingUuid) { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + pendingConnections.delete(meetingUuid); + reject(new Error( + `RTMS webhook not received within ${config.webhook_wait_timeout_ms}ms. ` + + `Possible causes: ` + + `(1) Meeting hasn't started, ` + + `(2) RTMS not enabled for this meeting, ` + + `(3) Webhook endpoint unreachable.` + )); + }, config.webhook_wait_timeout_ms); + + pendingConnections.set(meetingUuid, { + resolve: (rtmsInfo) => { + clearTimeout(timeoutId); + resolve(rtmsInfo); + }, + reject + }); + }); +} + +// Usage +try { + console.log('[RTMS] Waiting for webhook...'); + const rtmsInfo = await waitForRTMSWebhook(MEETING_UUID); + console.log('[RTMS] Webhook received, connecting...'); + await connectToRTMS(rtmsInfo); +} catch (error) { + console.error('[RTMS] ABORT:', error.message); +} +``` + +**Error if no webhook:** ABORT. No webhook = RTMS unavailable. No polling alternative. + +## Step 3: Connect to Signaling WebSocket + +### Connection with Retry + +```javascript +const WebSocket = require('ws'); + +async function connectSignalingWithRetry(signalingUrl, sessionKey) { + for (let attempt = 1; attempt <= config.connection_max_attempts; attempt++) { + console.log(`[SIGNALING] Attempt ${attempt}/${config.connection_max_attempts}`); + + try { + const ws = await connectSignalingSocket(signalingUrl, sessionKey); + console.log('[SIGNALING] Connected successfully'); + return ws; + } catch (error) { + console.error(`[SIGNALING] Attempt ${attempt} failed:`, error.message); + + if (attempt < config.connection_max_attempts) { + const delay = config.connection_retry_delay_ms; + console.log(`[SIGNALING] Retrying in ${delay}ms...`); + await sleep(delay); + } + } + } + + throw new Error( + `Failed to connect signaling WebSocket after ${config.connection_max_attempts} attempts` + ); +} + +function connectSignalingSocket(signalingUrl, sessionKey) { + return new Promise((resolve, reject) => { + const ws = new WebSocket(signalingUrl); + const timeoutId = setTimeout(() => { + ws.close(); + reject(new Error('Signaling connection timeout')); + }, config.connection_timeout_ms); + + ws.on('open', () => { + console.log('[SIGNALING] WebSocket opened, sending handshake...'); + + // Generate HMAC signature + const timestamp = Date.now(); + const message = `${timestamp}:${sessionKey}`; + const signature = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + // Send handshake + ws.send(JSON.stringify({ + type: 'handshake', + timestamp, + signature + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.type === 'handshake_response') { + clearTimeout(timeoutId); + + if (msg.status === 'success') { + console.log('[SIGNALING] Handshake successful'); + resolve(ws); + } else { + ws.close(); + reject(new Error(`Handshake failed: ${msg.error}`)); + } + } + }); + + ws.on('error', (error) => { + clearTimeout(timeoutId); + reject(error); + }); + + ws.on('close', (code, reason) => { + clearTimeout(timeoutId); + reject(new Error(`Connection closed: ${code} ${reason}`)); + }); + }); +} +``` + +## Step 4: Connect to Media WebSocket + +```javascript +async function connectMediaWithRetry(mediaUrl, signalingWs) { + for (let attempt = 1; attempt <= config.connection_max_attempts; attempt++) { + console.log(`[MEDIA] Attempt ${attempt}/${config.connection_max_attempts}`); + + try { + const ws = await connectMediaSocket(mediaUrl); + console.log('[MEDIA] Connected successfully'); + subscribeToStreams(ws); + setupKeepAlive(ws); + return ws; + } catch (error) { + console.error(`[MEDIA] Attempt ${attempt} failed:`, error.message); + + if (attempt < config.connection_max_attempts) { + const delay = config.connection_retry_delay_ms; + console.log(`[MEDIA] Retrying in ${delay}ms...`); + await sleep(delay); + } + } + } + + throw new Error( + `Failed to connect media WebSocket after ${config.connection_max_attempts} attempts` + ); +} + +function subscribeToStreams(mediaWs) { + // Subscribe to all available streams + mediaWs.send(JSON.stringify({ + type: 'subscribe', + streams: ['audio', 'video', 'transcription', 'share', 'chat'] + })); + + console.log('[MEDIA] Subscribed to: audio, video, transcription, share, chat'); +} +``` + +## Step 5: Keep-Alive Management + +```javascript +function setupKeepAlive(ws) { + let lastPongReceived = Date.now(); + let keepAliveInterval; + let timeoutCheck; + + // Send ping periodically + keepAliveInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + console.log('[KEEPALIVE] Ping sent'); + } + }, config.keepalive_interval_ms); + + // Check for pong timeout + timeoutCheck = setInterval(() => { + const timeSinceLastPong = Date.now() - lastPongReceived; + + if (timeSinceLastPong > config.keepalive_timeout_ms) { + console.error('[KEEPALIVE] Pong timeout, closing connection'); + clearInterval(keepAliveInterval); + clearInterval(timeoutCheck); + ws.close(1000, 'Keep-alive timeout'); + } + }, 1000); + + ws.on('pong', () => { + lastPongReceived = Date.now(); + console.log('[KEEPALIVE] Pong received'); + }); + + ws.on('close', () => { + clearInterval(keepAliveInterval); + clearInterval(timeoutCheck); + }); +} +``` + +## Step 6: Mid-Stream Reconnection + +```javascript +class ResilientRTMSConnection { + constructor(rtmsInfo, config) { + this.rtmsInfo = rtmsInfo; + this.config = config; + this.reconnectionAttempt = 0; + this.signalingWs = null; + this.mediaWs = null; + } + + async connect() { + try { + this.signalingWs = await connectSignalingWithRetry( + this.rtmsInfo.signalingUrl, + this.rtmsInfo.sessionKey + ); + + this.mediaWs = await connectMediaWithRetry( + this.rtmsInfo.mediaUrl, + this.signalingWs + ); + + this.setupReconnectionHandlers(); + + } catch (error) { + console.error('[RTMS] Initial connection failed:', error); + throw error; + } + } + + setupReconnectionHandlers() { + const handleDisconnection = (wsType) => async (code, reason) => { + console.error(`[${wsType}] Disconnected: ${code} ${reason}`); + + this.reconnectionAttempt++; + + if (this.reconnectionAttempt > this.config.reconnect_max_attempts) { + console.error( + `[RECONNECT] Giving up after ${this.reconnectionAttempt} attempts` + ); + this.cleanup(); + return; + } + + // Exponential backoff: 2s, 4s, 8s... + const delay = this.config.reconnect_base_delay_ms + * Math.pow(2, this.reconnectionAttempt - 1); + + console.log( + `[RECONNECT] Attempt ${this.reconnectionAttempt}/` + + `${this.config.reconnect_max_attempts} in ${delay}ms...` + ); + + await sleep(delay); + + try { + await this.connect(); + console.log('[RECONNECT] Successfully reconnected'); + this.reconnectionAttempt = 0; // Reset counter + } catch (error) { + console.error('[RECONNECT] Failed:', error.message); + // Handler will be called again if connection fails + } + }; + + this.signalingWs.on('close', handleDisconnection('SIGNALING')); + this.mediaWs.on('close', handleDisconnection('MEDIA')); + + this.signalingWs.on('error', (error) => { + console.error('[SIGNALING] Error:', error.message); + }); + + this.mediaWs.on('error', (error) => { + console.error('[MEDIA] Error:', error.message); + }); + } + + cleanup() { + if (this.signalingWs) this.signalingWs.close(); + if (this.mediaWs) this.mediaWs.close(); + } +} +``` + +### Customizing Reconnection Behavior + +```javascript +// Example: Capped exponential backoff (max 30s) +const delay = Math.min( + config.reconnect_base_delay_ms * Math.pow(2, reconnectionAttempt - 1), + 30000 // Cap at 30s +); + +// Example: Linear backoff instead of exponential +const delay = config.reconnect_base_delay_ms * reconnectionAttempt; + +// Example: Jittered backoff (avoid thundering herd) +const baseDelay = config.reconnect_base_delay_ms * Math.pow(2, reconnectionAttempt - 1); +const jitter = Math.random() * 1000; // Random 0-1000ms +const delay = baseDelay + jitter; +``` + +## Complete Resilient Bot Example + +```javascript +const config = loadConfig(); + +async function main() { + try { + // 1. Optional: Trigger RTMS via REST API + console.log('[RTMS] Triggering RTMS start...'); + await triggerRTMSStart(MEETING_ID); + + // 2. Wait for webhook + console.log('[RTMS] Waiting for meeting.rtms_started webhook...'); + const rtmsInfo = await waitForRTMSWebhook(MEETING_UUID); + + // 3. Connect with resilience + const rtms = new ResilientRTMSConnection(rtmsInfo, config); + await rtms.connect(); + + console.log('[RTMS] Bot is running, processing streams...'); + + // 4. Process media data + rtms.mediaWs.on('message', (data) => { + const frame = parseMediaFrame(data); + processMediaFrame(frame); + }); + + // 5. Handle graceful shutdown + process.on('SIGINT', () => { + console.log('[RTMS] Shutting down...'); + rtms.cleanup(); + process.exit(0); + }); + + } catch (error) { + console.error('[RTMS] ABORT:', error.message); + process.exit(1); + } +} + +main(); +``` + +## Comparison: RTMS Bot vs Meeting SDK Bot + +| Aspect | RTMS Bot | Meeting SDK Bot | +|--------|----------|-----------------| +| **Visibility** | Invisible (read-only service) | Visible participant | +| **Authentication** | REST API trigger + webhook | JWT + OBF token | +| **Join Dependency** | No dependency on participants | Owner must be present | +| **Retry Logic** | Not applicable (webhook-based) | Required (owner presence) | +| **Media Access** | Audio/video/text/share/chat via WebSocket | Raw audio/video/share via SDK | +| **Recording Control** | None (read-only) | Full (local, cloud, raw) | +| **Interaction** | Cannot interact | Can send chat, reactions | +| **Resource Usage** | Lower (WebSocket only) | Higher (full SDK) | +| **Use Case** | Passive transcription, analytics | Interactive bots, recording, moderation | + +**Choose RTMS Bot when:** +- You only need to observe/transcribe +- You want minimal resource usage +- You prefer invisible operation +- You're processing external meetings (with permission) + +**Choose Meeting SDK Bot when:** +- You need to interact with the meeting (chat, reactions) +- You need local recording control +- You want to be visible in participant list +- You're processing your own meetings + +## Troubleshooting + +### Webhook Never Arrives + +**Symptom:** `waitForRTMSWebhook()` times out + +**Solution:** +1. Verify webhook endpoint is HTTPS and publicly accessible +2. Check Event Subscriptions in Zoom Marketplace: `meeting.rtms_started` enabled +3. Verify RTMS was actually started (check Zoom client or REST API response) +4. Increase `webhook_wait_timeout_ms` if meeting starts later than expected +5. Test webhook delivery: `curl -X POST YOUR_WEBHOOK_URL` + +### Signaling Handshake Fails + +**Symptom:** Connection closes immediately after handshake + +**Solution:** +1. Verify HMAC signature generation matches Zoom docs +2. Check timestamp is current (not stale) +3. Verify `WEBHOOK_SECRET_TOKEN` matches Zoom Marketplace config +4. Check signaling URL hasn't expired (short TTL) + +### Keep-Alive Timeout + +**Symptom:** Connection closes with "Keep-alive timeout" + +**Solution:** +1. Network congestion - increase `keepalive_timeout_ms` +2. Server overloaded - increase `keepalive_interval_ms` +3. Verify ping/pong implementation is correct +4. Check firewall/proxy not blocking WebSocket pings + +### Frequent Reconnections + +**Symptom:** Bot reconnects multiple times, then gives up + +**Solution:** +1. Increase `reconnect_max_attempts` (e.g., 5 instead of 3) +2. Increase `reconnect_base_delay_ms` if network is slow +3. Monitor server resources (CPU/memory/network) +4. Check for rate limiting (too many connection attempts) + +## Resources + +- **RTMS Docs**: https://developers.zoom.us/docs/rtms/ +- **RTMS WebSocket Guide**: https://developers.zoom.us/docs/api/websockets/ +- **RTMS SDK**: https://github.com/zoom/rtms +- **Webhook Reference**: [../references/webhooks.md](../references/webhooks.md) +- **Connection Architecture**: [../concepts/connection-architecture.md](../concepts/connection-architecture.md) +- **Meeting SDK Bot Alternative**: [Meeting SDK Bot (Linux)](../../meeting-sdk/linux/meeting-sdk-bot.md) diff --git a/partner-built/zoom-plugin/skills/rtms/examples/sdk-quickstart.md b/partner-built/zoom-plugin/skills/rtms/examples/sdk-quickstart.md new file mode 100644 index 00000000..8518b109 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/examples/sdk-quickstart.md @@ -0,0 +1,365 @@ +# SDK Quickstart + +The fastest way to receive RTMS media using the official `@zoom/rtms` SDK. + +## Installation + +```bash +# Requires Node.js 20.3.0+ (24 LTS recommended) +npm install @zoom/rtms express +``` + +## Environment Setup + +```bash +# .env +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret +``` + +## Multi-Product Support + +The SDK accepts both `meeting_uuid` (meetings/webinars) and `session_id` (Video SDK) via `client.join(payload)` transparently. You only need to handle the different webhook event names -- the rest of the protocol is identical. + +```javascript +// These constants cover all RTMS products +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"]; +``` + +## Minimal Example + +```javascript +import rtms from "@zoom/rtms"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Handle webhook events - SDK starts webhook server automatically +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((data, timestamp, metadata) => { + const text = data.toString('utf8'); + console.log(`${metadata.userName}: ${text}`); + }); + + // SDK handles all WebSocket complexity + // Accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Complete Example with All Media Types + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"]; + +const clients = new Map(); + +rtms.onWebhookEvent(({ event, payload }) => { + const streamId = payload?.rtms_stream_id; + + // Handle session end (meetings, webinars, and Video SDK) + if (RTMS_STOP_EVENTS.includes(event)) { + const client = clients.get(streamId); + if (client) { + client.leave(); + clients.delete(streamId); + } + return; + } + + if (!RTMS_EVENTS.includes(event)) return; + + // Prevent duplicate connections + if (clients.has(streamId)) { + console.log('Already connected to this stream'); + return; + } + + const client = new rtms.Client(); + clients.set(streamId, client); + + // Join confirmation + client.onJoinConfirm((reason) => { + console.log(`Joined meeting: ${reason}`); + }); + + // Audio data + client.onAudioData((buffer, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}: ${buffer.length} bytes`); + // Save to file, send to transcription service, etc. + }); + + // Video data + client.onVideoData((buffer, timestamp, trackId, metadata) => { + console.log(`Video from ${metadata.userName}: ${buffer.length} bytes`); + // H.264 NAL units or JPG/PNG frames + }); + + // Transcript (real-time speech-to-text from Zoom) + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + console.log(`[${metadata.userName}]: ${text}`); + }); + + // Chat messages + client.onChatData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + console.log(`[Chat] ${metadata.userName}: ${text}`); + }); + + // Screen share + client.onShareData((buffer, timestamp, metadata) => { + console.log(`Screen share from ${metadata.userName}: ${buffer.length} bytes`); + }); + + // Participant events + client.onParticipantEvent((event, timestamp, participants) => { + participants.forEach(p => { + console.log(`Participant ${event}: ${p.userName}`); + }); + }); + + // Active speaker changed + client.onActiveSpeakerEvent((timestamp, userId, userName) => { + console.log(`Active speaker: ${userName}`); + }); + + // Screen sharing started/stopped + client.onSharingEvent((event, timestamp, userId, userName) => { + console.log(`Sharing ${event}: ${userName}`); + }); + + // Session ended + client.onLeave((reason) => { + console.log(`Left meeting: ${reason}`); + clients.delete(streamId); + }); + + // Join the meeting + client.join(payload); +}); +``` + +## Configuring Audio Parameters + +```javascript +import rtms from "@zoom/rtms"; + +const client = new rtms.Client(); + +// Set audio parameters before joining +client.setAudioParams({ + contentType: 2, // RAW_AUDIO + codec: 4, // OPUS (default) + sampleRate: 3, // 48kHz + channel: 2, // Stereo (only with OPUS) + dataOpt: 2, // AUDIO_MULTI_STREAMS (per-participant) + duration: 20, // 20ms chunks + frameSize: 960 // Samples per frame +}); + +client.join(payload); +``` + +### Audio Parameter Options + +| Parameter | Options | +|-----------|---------| +| `contentType` | 1=RTP, 2=RAW_AUDIO | +| `codec` | 1=L16 (PCM), 2=G.711, 3=G.722, 4=OPUS | +| `sampleRate` | 0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz | +| `channel` | 1=Mono, 2=Stereo (OPUS only!) | +| `dataOpt` | 1=Mixed stream, 2=Multi-streams (per participant) | +| `duration` | Chunk size in ms (multiple of 20, max 1000) | + +## Configuring Video Parameters + +```javascript +client.setVideoParams({ + contentType: 3, // RAW_VIDEO + codec: 7, // H.264 + resolution: 2, // HD (720p) + fps: 25, + dataOpt: 3 // Single active speaker +}); +``` + +### Video Parameter Options + +| Parameter | Options | +|-----------|---------| +| `codec` | 5=JPG, 6=PNG, 7=H.264 | +| `resolution` | 1=SD (480p), 2=HD (720p), 3=FHD (1080p), 4=QHD (1440p) | +| `fps` | 1-30 (JPG/PNG max 5, H.264 max 30) | +| `dataOpt` | 3=Single active speaker | + +## With Express Webhook Handler + +```javascript +import rtms from "@zoom/rtms"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Use SDK's webhook handler +app.post('/webhook', rtms.createWebhookHandler(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((data, timestamp, metadata) => { + console.log(`${metadata.userName}: ${data.toString('utf8')}`); + }); + + client.join(payload); +}, '/webhook')); + +app.listen(3000, () => { + console.log('Server running on port 3000'); +}); +``` + +## Class-Based Approach (Multiple Connections) + +For applications needing multiple concurrent connections: + +```javascript +import rtms from "@zoom/rtms"; + +// Initialize SDK once +rtms.Client.initialize(); + +// Create multiple clients +const client1 = new rtms.Client(); +const client2 = new rtms.Client(); + +client1.onTranscriptData((data, ts, meta) => { + console.log(`[Meeting 1] ${meta.userName}: ${data.toString('utf8')}`); +}); + +client2.onTranscriptData((data, ts, meta) => { + console.log(`[Meeting 2] ${meta.userName}: ${data.toString('utf8')}`); +}); + +// Join different meetings +client1.join(meeting1Payload); +client2.join(meeting2Payload); +``` + +## Error Handling + +```javascript +client.onJoinConfirm((reason) => { + if (reason !== 0) { + console.error(`Join failed with reason: ${reason}`); + // Handle error + } +}); + +client.onLeave((reason) => { + console.log(`Left meeting with reason: ${reason}`); + + // Cleanup + clients.delete(streamId); + + // Optionally reconnect + if (reason === /* unexpected disconnect */) { + setTimeout(() => reconnect(), 2000); + } +}); +``` + +## Python SDK + +```python +import rtms +from dotenv import load_dotenv + +load_dotenv() + +RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started'] +RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped'] + +clients = {} + +@rtms.onWebhookEvent +def handle_webhook(webhook): + event = webhook.get('event') + payload = webhook.get('payload', {}) + stream_id = payload.get('rtms_stream_id') + + if event in RTMS_STOP_EVENTS: + if stream_id in clients: + clients[stream_id].leave() + del clients[stream_id] + return + + if event not in RTMS_EVENTS: + return + + client = rtms.Client() + clients[stream_id] = client + + @client.onTranscriptData + def on_transcript(data, size, timestamp, metadata): + text = data.decode('utf-8') + print(f'[{metadata.userName}]: {text}') + + @client.onJoinConfirm + def on_join(reason): + print(f'Joined: {reason}') + + @client.onLeave + def on_leave(reason): + print(f'Left: {reason}') + + # SDK accepts both meeting_uuid and session_id transparently + client.join(payload) + +# Main loop +if __name__ == '__main__': + print('Webhook server running...') + rtms.run() +``` + +## Environment Variables Reference + +```bash +# Required +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret + +# Optional +ZM_RTMS_PORT=8080 # Webhook server port +ZM_RTMS_PATH=/webhook # Webhook endpoint path + +# Logging +ZM_RTMS_LOG_LEVEL=info # error, warn, info, debug, trace +ZM_RTMS_LOG_FORMAT=progressive # progressive or json +ZM_RTMS_LOG_ENABLED=true +``` + +## Common Issues + +| Issue | Solution | +|-------|----------| +| Segmentation fault | Upgrade to Node.js 20.3.0+ (24 LTS recommended) | +| Audio metadata missing userId | Use `onActiveSpeakerEvent` for speaker identification with mixed stream | +| Video params ignored | Call `setVideoParams` BEFORE `setAudioParams` | + +## Next Steps + +- **[Manual WebSocket](manual-websocket.md)** - Full protocol control without SDK +- **[AI Integration](ai-integration.md)** - Transcription and analysis patterns +- **[Media Types](../references/media-types.md)** - All configuration options diff --git a/partner-built/zoom-plugin/skills/rtms/references/connection.md b/partner-built/zoom-plugin/skills/rtms/references/connection.md new file mode 100644 index 00000000..f26126fd --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/connection.md @@ -0,0 +1,276 @@ +# RTMS - Connection + +WebSocket connection protocol details. + +## Connection Flow + +``` +1. Receive meeting/webinar/session.rtms_started webhook + ↓ +2. Extract server_urls, stream_id, and meeting_uuid or session_id + ↓ +3. Generate signature (HMAC-SHA256) using meeting_uuid or session_id + ↓ +4. Connect to signaling WebSocket + ↓ +5. Send handshake request (msg_type 1) + ↓ +6. Receive handshake response (msg_type 2) with media server URL + ↓ +7. Connect to media WebSocket(s) + ↓ +8. Send media handshake (msg_type 3) + ↓ +9. Receive media handshake response (msg_type 4) + ↓ +10. Send ready to receive (msg_type 7) + ↓ +11. Receive media data (msg_type 14-18) + ↓ +12. Respond to heartbeats (msg_type 12 → 13) + ↓ +13. Optionally react to `PARTICIPANT_VIDEO_ON/OFF`, send `VIDEO_SUBSCRIPTION_REQ`, or gracefully terminate with `STREAM_CLOSE_REQ` +``` + +## Signature Generation + +```javascript +const crypto = require('crypto'); + +// For meetings and webinars: use meeting_uuid +// For Video SDK: use session_id +// Webinars still use meeting_uuid (NOT webinar_uuid) +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret).update(message).digest('hex'); +} + +// Extract the correct ID from any product's webhook payload +const idValue = payload.meeting_uuid || payload.session_id; +``` + +## Signaling Message Types + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 1 | Handshake Request | Client → Server | Initiate connection | +| 2 | Handshake Response | Server → Client | Returns media server URL | +| 3 | Media Handshake Request | Client → Server | Request specific media types | +| 4 | Media Handshake Response | Server → Client | Confirms media subscription | +| 7 | Ready to Receive | Client → Server | Signal ready for data | +| 12 | Keep Alive Request | Server → Client | Heartbeat ping | +| 13 | Keep Alive Response | Client → Server | Heartbeat pong | + +## Media Message Types + +| msg_type | Media Type | +|----------|------------| +| 14 | Audio | +| 15 | Video | +| 16 | Screen Share | +| 17 | Transcript | +| 18 | Chat | + +## Critical Gotchas + +### 1. Only ONE Connection Per Stream! + +```javascript +// WRONG - Connecting twice kicks out first connection +connectToRTMS(serverUrl, streamId); // Connection 1 +connectToRTMS(serverUrl, streamId); // Connection 2 - kicks out Connection 1! + +// CORRECT - Only connect once +if (!activeConnections.has(streamId)) { + connectToRTMS(serverUrl, streamId); + activeConnections.set(streamId, ws); +} +``` + +### 2. Heartbeat is MANDATORY + +When you receive msg_type 12, you MUST respond with msg_type 13: + +```javascript +ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 12) { // Keep Alive Request + ws.send(JSON.stringify({ + msg_type: 13, // Keep Alive Response + timestamp: msg.timestamp + })); + } +}); +``` + +### 3. Reconnection is YOUR Responsibility + +RTMS does NOT auto-reconnect. Implement your own retry logic: + +| Server Type | Timeout | +|-------------|---------| +| Media Server | 65 seconds keep-alive tolerance before timeout | +| Signaling Server | 60 seconds to reconnect | + +```javascript +ws.on('close', () => { + // Implement exponential backoff + setTimeout(() => reconnect(), retryDelay); + retryDelay = Math.min(retryDelay * 2, 30000); +}); +``` + +## Transcript LID Control + +The transcript media handshake now supports explicit Language Identification control. + +```javascript +mediaWs.send(JSON.stringify({ + msg_type: 3, + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id: streamId, + signature, + media_type: 8, // TRANSCRIPT + media_params: { + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Lock to src_language instead of auto-switching + } + } +})); +``` + +Use `enable_lid: false` when: + +- the meeting should stay on a known language +- language-switching is undesirable +- you want more predictable downstream transcript processing + +## Single Individual Video Subscription Flow + +RTMS now supports subscribing to **one participant camera stream at a time**. + +1. Open a video media socket with `data_opt = VIDEO_SINGLE_INDIVIDUAL_STREAM` +2. Subscribe to `PARTICIPANT_VIDEO_ON` and `PARTICIPANT_VIDEO_OFF` +3. When an event arrives, choose the `user_id` you want +4. Send `VIDEO_SUBSCRIPTION_REQ` on the signaling socket +5. Wait for `VIDEO_SUBSCRIPTION_RESP` +6. Expect the newest successful subscription to replace the previous participant stream + +```javascript +// Signaling socket: subscribe to control-plane events +signalingWs.send(JSON.stringify({ + msg_type: 5, // EVENT_SUBSCRIPTION + events: [ + { event_type: 8, subscribe: true }, // PARTICIPANT_VIDEO_ON + { event_type: 9, subscribe: true } // PARTICIPANT_VIDEO_OFF + ] +})); + +// Signaling socket: select a participant stream +signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: selectedUserId, + subscribe: true, + timestamp: Date.now() +})); +``` + +The March 2026 changelog did not publish the numeric values for the new message types. Use the protocol definitions before hard-coding them. + +## Graceful Stream Closure + +The backend can now request clean shutdown over the signaling socket: + +```javascript +signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId +})); +``` + +Expect: + +- `STREAM_CLOSE_RESP` +- then normal connection shutdown / cleanup + +Use this when your app wants deterministic teardown instead of waiting for a stop webhook or socket failure. + +## Split vs Unified Mode + +| Mode | Description | Best For | +|------|-------------|----------| +| **Split** | One connection per media type | Most use cases. Media server supports multiple connections with different media types | +| **Unified** | One connection for all media | Real-time audio+video streaming/muxing where sync matters | + +## Low-Level Connection Example + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +async function connectRTMS(webhookPayload) { + const { server_urls, rtms_stream_id } = webhookPayload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = webhookPayload.meeting_uuid || webhookPayload.session_id; + + // Generate signature + const signature = crypto + .createHmac('sha256', process.env.ZOOM_CLIENT_SECRET) + .update(`${process.env.ZOOM_CLIENT_ID},${idValue},${rtms_stream_id}`) + .digest('hex'); + + // Connect to signaling server + const signalingWs = new WebSocket(server_urls, { + headers: { + 'X-Zoom-RTMS-Stream-Id': rtms_stream_id, + 'X-Zoom-RTMS-Signature': signature + } + }); + + signalingWs.on('open', () => { + // Send handshake request + signalingWs.send(JSON.stringify({ + msg_type: 1, + protocol_version: 1, + client_id: process.env.ZOOM_CLIENT_ID, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + stream_id: rtms_stream_id, + signature: signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + signalingWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 2: // Handshake response + // Connect to media server from msg.media_server_url + connectMediaServer(msg.media_server_url); + break; + case 12: // Keep alive request + signalingWs.send(JSON.stringify({ msg_type: 13, timestamp: msg.timestamp })); + break; + } + }); + + signalingWs.on('error', (error) => { + console.error('Signaling error:', error); + }); + + signalingWs.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason); + // Implement reconnection logic + }); +} +``` + +## Resources + +- **RTMS_CONNECTION_FLOW.md**: https://github.com/zoom/rtms-samples/blob/main/RTMS_CONNECTION_FLOW.md +- **ARCHITECTURE.md**: https://github.com/zoom/rtms-samples/blob/main/ARCHITECTURE.md +- **TROUBLESHOOTING.md**: https://github.com/zoom/rtms-samples/blob/main/TROUBLESHOOTING.md diff --git a/partner-built/zoom-plugin/skills/rtms/references/data-types.md b/partner-built/zoom-plugin/skills/rtms/references/data-types.md new file mode 100644 index 00000000..47b50d2d --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/data-types.md @@ -0,0 +1,322 @@ +# RTMS Data Types + +Complete reference for all RTMS enums and constants. + +## Message Types (RTMS_MESSAGE_TYPE) + +| Value | Name | Direction | Description | +|-------|------|-----------|-------------| +| 0 | UNDEFINED | - | Default | +| 1 | SIGNALING_HAND_SHAKE_REQ | Client -> Server | Signaling handshake request | +| 2 | SIGNALING_HAND_SHAKE_RESP | Server -> Client | Signaling handshake response | +| 3 | DATA_HAND_SHAKE_REQ | Client -> Server | Media handshake request | +| 4 | DATA_HAND_SHAKE_RESP | Server -> Client | Media handshake response | +| 5 | EVENT_SUBSCRIPTION | Client -> Server | Subscribe/unsubscribe events | +| 6 | EVENT_UPDATE | Server -> Client | Event notification | +| 7 | CLIENT_READY_ACK | Client -> Server | Ready to receive media | +| 8 | STREAM_STATE_UPDATE | Server -> Client | Stream state changed | +| 9 | SESSION_STATE_UPDATE | Server -> Client | Session state changed | +| 10 | SESSION_STATE_REQ | Client -> Server | Request session state | +| 11 | SESSION_STATE_RESP | Server -> Client | Session state response | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | +| 14 | MEDIA_DATA_AUDIO | Server -> Client | Audio data | +| 15 | MEDIA_DATA_VIDEO | Server -> Client | Video data | +| 16 | MEDIA_DATA_SHARE | Server -> Client | Screen share data | +| 17 | MEDIA_DATA_TRANSCRIPT | Server -> Client | Transcript data | +| 18 | MEDIA_DATA_CHAT | Server -> Client | Chat data | +| 19 | STREAM_STATE_REQ | Client -> Server | Request stream state | +| 20 | STREAM_STATE_RESP | Server -> Client | Stream state response | +| 21 | STREAM_CLOSE_REQ | Client -> Server | Request graceful stream shutdown | +| 22 | STREAM_CLOSE_RESP | Server -> Client | Response to close stream request | +| 23 | META_DATA_AUDIO | Server -> Client | Audio metadata | +| 24 | META_DATA_VIDEO | Server -> Client | Reserved video metadata | +| 25 | META_DATA_SHARE | Server -> Client | Reserved share metadata | +| 26 | META_DATA_TRANSCRIPT | Server -> Client | Reserved transcript metadata | +| 27 | META_DATA_CHAT | Server -> Client | Reserved chat metadata | +| 28 | VIDEO_SUBSCRIPTION_REQ | Client -> Server | Subscribe or unsubscribe one participant video stream | +| 29 | VIDEO_SUBSCRIPTION_RESP | Server -> Client | Response to participant video subscription | + +## Event Types (RTMS_EVENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | FIRST_PACKET_TIMESTAMP | First packet received | +| 2 | ACTIVE_SPEAKER_CHANGE | Active speaker changed | +| 3 | PARTICIPANT_JOIN | Participant joined | +| 4 | PARTICIPANT_LEAVE | Participant left | +| 5 | SHARING_START | Screen sharing started | +| 6 | SHARING_STOP | Screen sharing stopped | +| 7 | MEDIA_CONNECTION_INTERRUPTED | Connection interrupted | +| 8 | PARTICIPANT_VIDEO_ON | Participant camera turned on | +| 9 | PARTICIPANT_VIDEO_OFF | Participant camera turned off | + +## Zoom Contact Center Voice Event Types (RTMS_ZCC_VOICE_EVENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 8 | CONSUMER_ANSWERED | Consumer answered the call | +| 9 | CONSUMER_END | Consumer ended the call | +| 10 | USER_ANSWERED | User or agent answered the call | +| 11 | USER_END | User or agent ended the call | +| 12 | USER_HOLD | User placed the call on hold | +| 13 | USER_UNHOLD | User resumed the call | +| 14 | MONITOR_STARTED | Monitoring started | +| 15 | MONITOR_TRANSITIONED | Monitoring transitioned | +| 16 | MONITOR_ENDED | Monitoring ended | +| 17 | TAKEOVER_STARTED | Takeover started | +| 18 | TRANSFER_INITIATED | Transfer initiated | +| 19 | TRANSFER_CANCELED | Transfer canceled | +| 20 | TRANSFER_ACCEPTED | Transfer accepted | +| 21 | TRANSFER_COMPLETED | Transfer completed | +| 22 | TRANSFER_REJECTED | Transfer rejected | +| 23 | TRANSFER_TIMEOUT | Transfer timed out | +| 24 | CONFERENCE_CANCELED | Conference canceled | +| 25 | CONFERENCE_PARTICIPANT_CANCELED | Conference participant canceled | +| 26 | CONFERENCE_PARTICIPANT_INVITED | Conference participant invited | +| 27 | CONFERENCE_PARTICIPANT_REJECTED | Conference participant rejected | +| 28 | CONFERENCE_PARTICIPANT_TIMEOUT | Conference participant timed out | + +## Status Codes (RTMS_STATUS_CODE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | STATUS_OK | Success | +| 1 | STATUS_INVALID_MESSAGE_TYPE | Invalid message type | +| 2 | STATUS_INVALID_RTMS_STREAM_ID | Invalid RTMS stream ID | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 4 | STATUS_INVALID_PAYLOAD | Invalid payload | +| 5 | STATUS_INVALID_EVENTS | Invalid event list | +| 6 | STATUS_INVALID_EVENT_TYPE | Invalid event type | +| 7 | STATUS_INVALID_MEDIA_TYPE | Invalid media type | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Duplicate signaling connection | +| 9 | STATUS_MEDIA_TYPE_AUDIO_NOT_SUPPORT | Audio stream not supported | +| 10 | STATUS_MEDIA_TYPE_VIDEO_NOT_SUPPORT | Video stream not supported | +| 11 | STATUS_MEDIA_TYPE_DESKSHARE_NOT_SUPPORT | Screen share stream not supported | +| 12 | STATUS_MEDIA_TYPE_TRANSCRIPT_NOT_SUPPORT | Transcript stream not supported | +| 13 | STATUS_MEDIA_TYPE_CHAT_NOT_SUPPORT | Chat stream not supported | +| 14 | STATUS_MEDIA_TYPE_INVALID_VALUE | Invalid media type value | +| 15 | STATUS_MEDIA_DATA_ALL_CONNECTION_EXIST | All media data connections already exist | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Duplicate media data connection | +| 17 | STATUS_INVALID_MEDIA_PARAMS | Invalid media params | +| 18 | STATUS_INVALID_MEDIA_AUDIO_PARAMS | Invalid audio params | +| 19 | STATUS_INVALID_MEDIA_AUDIO_CONTENT_TYPE | Invalid audio content type | +| 20 | STATUS_INVALID_MEDIA_AUDIO_SAMPLE_RATE | Invalid audio sample rate | +| 21 | STATUS_INVALID_MEDIA_AUDIO_CHANNEL | Invalid audio channel count | +| 22 | STATUS_INVALID_MEDIA_AUDIO_CODEC | Invalid audio codec | +| 23 | STATUS_INVALID_MEDIA_AUDIO_DATA_OPT | Invalid audio data option | +| 24 | STATUS_INVALID_MEDIA_AUDIO_SEND_RATE | Invalid audio send rate | +| 25 | STATUS_INVALID_MEDIA_VIDEO_PARAMS | Invalid video params | +| 26 | STATUS_INVALID_MEDIA_VIDEO_CONTENT_TYPE | Invalid video content type | +| 27 | STATUS_INVALID_MEDIA_VIDEO_CODEC | Invalid video codec | +| 28 | STATUS_INVALID_MEDIA_VIDEO_RESOLUTION | Invalid video resolution | +| 29 | STATUS_INVALID_MEDIA_VIDEO_DATA_OPT | Invalid video data option | +| 30 | STATUS_INVALID_MEDIA_VIDEO_FPS | Invalid video FPS | +| 31 | STATUS_INVALID_MEDIA_DESKSHARE_PARAMS | Invalid deskshare params | +| 32 | STATUS_INVALID_MEDIA_DESKSHARE_CONTENT_TYPE | Invalid deskshare content type | +| 33 | STATUS_INVALID_MEDIA_DESKSHARE_CODEC | Invalid deskshare codec | +| 34 | STATUS_INVALID_MEDIA_DESKSHARE_RESOLUTION | Invalid deskshare resolution | +| 35 | STATUS_INVALID_MEDIA_DESKSHARE_FPS | Invalid deskshare FPS | +| 36 | STATUS_INVALID_MEDIA_TRANSCRIPT_PARAMS | Invalid transcript params | +| 37 | STATUS_INVALID_MEDIA_TRANSCRIPT_CONTENT_TYPE | Invalid transcript content type | +| 38 | STATUS_INVALID_MEDIA_CHAT_PARAMS | Invalid chat params | +| 39 | STATUS_INVALID_MEDIA_CHAT_CONTENT_TYPE | Invalid chat content type | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 41 | STATUS_INVALID_CLIENT_READY_ACK | Invalid client ready ack | +| 42 | STATUS_INVALID_EVENT_SUBSCRIBE | Invalid event subscription payload | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +## Media Data Types (MEDIA_DATA_TYPE) + +Use bitwise OR to combine: + +| Value | Name | Constant | +|-------|------|----------| +| 1 | AUDIO | 0x01 | +| 2 | VIDEO | 0x01 << 1 | +| 4 | DESKSHARE | 0x01 << 2 | +| 8 | TRANSCRIPT | 0x01 << 3 | +| 16 | CHAT | 0x01 << 4 | +| 32 | ALL | 0x01 << 5 | + +**Common combinations**: +- Audio + Transcript: `1 | 8 = 9` +- Audio + Video: `1 | 2 = 3` +- All media: `32` + +## Content Types (MEDIA_CONTENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | RTP | Real-time audio/video | +| 2 | RAW_AUDIO | Raw audio data | +| 3 | RAW_VIDEO | Raw video data | +| 4 | FILE_STREAM | File stream | +| 5 | TEXT | Text (transcript, chat) | + +## Payload Types / Codecs (MEDIA_PAYLOAD_TYPE) + +| Value | Name | Use | +|-------|------|-----| +| 0 | UNDEFINED | - | +| 1 | L16 | Audio - PCM uncompressed | +| 2 | G711 | Audio - compressed | +| 3 | G722 | Audio - compressed | +| 4 | OPUS | Audio - compressed | +| 5 | JPG | Video/Share - when fps <= 5 | +| 6 | PNG | Video/Share - when fps <= 5 | +| 7 | H264 | Video/Share - when fps > 5 | + +## Media Data Options (MEDIA_DATA_OPTION) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | - | +| 1 | AUDIO_MIXED_STREAM | All audio combined | +| 2 | AUDIO_MULTI_STREAMS | Per-participant audio | +| 3 | VIDEO_SINGLE_ACTIVE_STREAM | Active speaker video | +| 4 | VIDEO_SINGLE_INDIVIDUAL_STREAM | One manually selected participant video stream | + +## Media Resolution (MEDIA_RESOLUTION) + +| Value | Name | Pixels | +|-------|------|--------| +| 1 | SD | 854x480 or 640x360 | +| 2 | HD | 1280x720 | +| 3 | FHD | 1920x1080 | +| 4 | QHD | 2560x1440 | + +## Audio Sample Rate (AUDIO_SAMPLE_RATE) + +| Value | Name | Rate | +|-------|------|------| +| 0 | SR_8K | 8000 Hz | +| 1 | SR_16K | 16000 Hz | +| 2 | SR_32K | 32000 Hz | +| 3 | SR_48K | 48000 Hz | + +## Audio Channel (AUDIO_CHANNEL) + +| Value | Name | Note | +|-------|------|------| +| 1 | MONO | Default | +| 2 | STEREO | Only with OPUS codec! | + +## Session State (RTMS_SESSION_STATE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | INACTIVE | Default | +| 1 | INITIALIZE | Session initializing | +| 2 | STARTED | Session started | +| 3 | PAUSED | Session paused | +| 4 | RESUMED | Session resumed | +| 5 | STOPPED | Session stopped | + +## Stream State (RTMS_STREAM_STATE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | INACTIVE | Default | +| 1 | ACTIVE | Streaming | +| 2 | INTERRUPTED | Connection problem | +| 3 | TERMINATING | Ending | +| 4 | TERMINATED | Ended | +| 5 | PAUSED | Paused | +| 6 | RESUMED | Resumed | + +## Stop Reasons (RTMS_STOP_REASON) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | STOP_BC_HOST_TRIGGERED | Host stopped | +| 2 | STOP_BC_USER_TRIGGERED | User stopped | +| 3 | STOP_BC_USER_LEFT | User left meeting | +| 4 | STOP_BC_USER_EJECTED | User ejected by host | +| 5 | STOP_BC_HOST_DISABLED_APP | Host disabled app | +| 6 | STOP_BC_MEETING_ENDED | Meeting ended | +| 7 | STOP_BC_STREAM_CANCELED | Stream canceled | +| 8 | STOP_BC_STREAM_REVOKED | Stream revoked (delete assets!) | +| 9 | STOP_BC_ALL_APPS_DISABLED | All apps disabled | +| 10 | STOP_BC_INTERNAL_EXCEPTION | Internal error | +| 11 | STOP_BC_CONNECTION_TIMEOUT | Connection timeout | +| 12 | STOP_BC_INSTANCE_CONNECTION_INTERRUPTED | Instance/media connection interrupted | +| 13 | STOP_BC_SIGNAL_CONNECTION_INTERRUPTED | Signaling issue | +| 14 | STOP_BC_DATA_CONNECTION_INTERRUPTED | Data connection issue | +| 15 | STOP_BC_SIGNAL_CONNECTION_CLOSED_ABNORMALLY | Abnormal close | +| 16 | STOP_BC_DATA_CONNECTION_CLOSED_ABNORMALLY | Abnormal close | +| 17 | STOP_BC_EXIT_SIGNAL | Exit signal received | +| 18 | STOP_BC_AUTHENTICATION_FAILURE | Auth failed | +| 19 | STOP_BC_AWAIT_RECONNECTION_TIMEOUT | Awaited reconnection timed out | +| 20 | STOP_BC_RECEIVER_REQUEST_CLOSE | Receiver requested stream close | +| 21 | STOP_BC_CUSTOMER_DISCONNECTED | Contact Center customer disconnected | +| 22 | STOP_BC_AGENT_DISCONNECTED | Contact Center agent disconnected | +| 23 | STOP_BC_ADMIN_DISABLED_APP | Admin disabled app | +| 24 | STOP_BC_KEEP_ALIVE_TIMEOUT | Three keep-alive requests missed | +| 25 | STOP_BC_MANUAL_API_TRIGGERED | ZCC Voice API triggered stop | +| 26 | STOP_BC_STREAMING_NOT_SUPPORTED | Queue does not support streaming | + +## Transcript Languages (RTMS_TRANSCRIPT_LANGUAGE) + +| Value | Name | Language | +|-------|------|----------| +| -1 | LANGUAGE_ID_NONE | None/Auto-detect | +| 0 | LANGUAGE_ID_ARABIC | Arabic | +| 1 | LANGUAGE_ID_BENGALI | Bengali | +| 2 | LANGUAGE_ID_CANTONESE | Cantonese | +| 3 | LANGUAGE_ID_CATALAN | Catalan | +| 4 | LANGUAGE_ID_CHINESE_SIMPLIFIED | Chinese (Simplified) | +| 5 | LANGUAGE_ID_CHINESE_TRADITIONAL | Chinese (Traditional) | +| 6 | LANGUAGE_ID_CZECH | Czech | +| 7 | LANGUAGE_ID_DANISH | Danish | +| 8 | LANGUAGE_ID_DUTCH | Dutch | +| **9** | **LANGUAGE_ID_ENGLISH** | **English (Default)** | +| 10 | LANGUAGE_ID_ESTONIAN | Estonian | +| 11 | LANGUAGE_ID_FINNISH | Finnish | +| 12 | LANGUAGE_ID_FRENCH_CANADA | French (Canada) | +| 13 | LANGUAGE_ID_FRENCH_FRANCE | French (France) | +| 14 | LANGUAGE_ID_GERMAN | German | +| 15 | LANGUAGE_ID_HEBREW | Hebrew | +| 16 | LANGUAGE_ID_HINDI | Hindi | +| 17 | LANGUAGE_ID_HUNGARIAN | Hungarian | +| 18 | LANGUAGE_ID_INDONESIAN | Indonesian | +| 19 | LANGUAGE_ID_ITALIAN | Italian | +| 20 | LANGUAGE_ID_JAPANESE | Japanese | +| 21 | LANGUAGE_ID_KOREAN | Korean | +| 22 | LANGUAGE_ID_MALAY | Malay | +| 23 | LANGUAGE_ID_PERSIAN | Persian | +| 24 | LANGUAGE_ID_POLISH | Polish | +| 25 | LANGUAGE_ID_PORTUGUESE | Portuguese | +| 26 | LANGUAGE_ID_ROMANIAN | Romanian | +| 27 | LANGUAGE_ID_RUSSIAN | Russian | +| 28 | LANGUAGE_ID_SPANISH | Spanish | +| 29 | LANGUAGE_ID_SWEDISH | Swedish | +| 30 | LANGUAGE_ID_TAGALOG | Tagalog | +| 31 | LANGUAGE_ID_TAMIL | Tamil | +| 32 | LANGUAGE_ID_TELUGU | Telugu | +| 33 | LANGUAGE_ID_THAI | Thai | +| 34 | LANGUAGE_ID_TURKISH | Turkish | +| 35 | LANGUAGE_ID_UKRAINIAN | Ukrainian | +| 36 | LANGUAGE_ID_VIETNAMESE | Vietnamese | + +## Transcript Handshake Controls + +Transcript media handshakes now use: + +- `src_language`: fixed requested transcription language +- `enable_lid`: boolean switch for Language Identification + +Behavior: + +- `enable_lid: true` or omitted: RTMS can automatically switch languages during transcription +- `enable_lid: false`: RTMS stays on `src_language` and does not auto-switch + +## Next Steps + +- **[Media Types](media-types.md)** - Parameter configurations +- **[Connection](connection.md)** - Protocol details +- **[Manual WebSocket](../examples/manual-websocket.md)** - Implementation diff --git a/partner-built/zoom-plugin/skills/rtms/references/environment-variables.md b/partner-built/zoom-plugin/skills/rtms/references/environment-variables.md new file mode 100644 index 00000000..14e80767 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/environment-variables.md @@ -0,0 +1,26 @@ +# Zoom RTMS Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | OAuth mode | RTMS subscription/auth (Meetings/Webinars mode) | Zoom Marketplace -> OAuth app credentials | +| `ZOOM_CLIENT_SECRET` | OAuth mode | RTMS subscription/auth (Meetings/Webinars mode) | Zoom Marketplace -> OAuth app credentials | +| `ZOOM_ACCOUNT_ID` | S2S OAuth mode | Account-level RTMS token grants | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_VIDEO_SDK_KEY` | Video SDK RTMS mode | RTMS with Video SDK sessions | Zoom Marketplace -> Video SDK app credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Video SDK RTMS mode | Video SDK session auth/signing | Zoom Marketplace -> Video SDK app credentials | +| `ZOOM_SECRET_TOKEN` or `WEBHOOK_SECRET_TOKEN` | Yes when validating events | Event signature verification | Zoom Marketplace -> Event Subscriptions -> Secret Token | + +## Connection tuning (optional) + +- `RTMS_CONNECTION_TIMEOUT_MS` +- `RTMS_CONNECTION_MAX_ATTEMPTS` +- `RTMS_CONNECTION_RETRY_DELAY_MS` +- `RTMS_RECONNECT_MAX_ATTEMPTS` +- `RTMS_RECONNECT_BASE_DELAY_MS` +- `RTMS_KEEPALIVE_INTERVAL_MS` +- `RTMS_KEEPALIVE_TIMEOUT_MS` + +## Notes + +- Choose one credential mode per deployment: OAuth or Video SDK credentials. diff --git a/partner-built/zoom-plugin/skills/rtms/references/media-types.md b/partner-built/zoom-plugin/skills/rtms/references/media-types.md new file mode 100644 index 00000000..d34dd7f0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/media-types.md @@ -0,0 +1,240 @@ +# RTMS - Media Types + +Audio, video, transcript, chat, and screen share data formats. + +## Media Type Bitmask + +Use bitwise OR to combine types: + +| Type | Value | Event Name | Description | +|------|-------|------------|-------------| +| Audio | 1 | `audio` | PCM audio samples | +| Video | 2 | `video` | H.264 encoded frames | +| Screen Share | 4 | `sharescreen` | **Separate from video!** | +| Transcript | 8 | `transcript` | Real-time speech-to-text | +| Chat | 16 | `chat` | In-meeting chat messages | +| All | 32 | all events | All media types | + +**Example**: Audio + Transcript = `1 | 8` = `9` + +```javascript +const mediaTypes = RTMSManager.MEDIA.AUDIO | RTMSManager.MEDIA.TRANSCRIPT; // 9 +``` + +## Audio + +| Property | Options | +|----------|---------| +| Sample Rate | 8kHz (0), **16kHz (1)**, 32kHz (2), 48kHz (3) | +| Codec | **L16/PCM (1)**, G.711 (2), G.722 (3), Opus (4) | +| Channels | **Mono (1)**, Stereo (2) | +| Data Option | **Mixed (1)**, Multi-stream (2) | +| Send Rate | **20ms** (recommended) | + +**Important**: Stereo is ONLY supported with Opus codec! + +### Audio Configuration Example + +```javascript +const audioParams = { + content_type: 1, // MEDIA_CONTENT_TYPE_RTP + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream (all participants) + send_rate: 20 // 20ms intervals +}; +``` + +### Processing Audio + +```javascript +RTMSManager.on('audio', ({ buffer, userName, timestamp }) => { + // buffer = PCM 16-bit samples + // Send to transcription service, save to file, etc. + transcriptionService.process(buffer); +}); +``` + +## Video + +| Property | Options | +|----------|---------| +| Codec | **H.264 (7)**, JPG (5), PNG (6) | +| Resolution | SD (1), **HD 720p (2)**, FHD 1080p (3), QHD 2K (4) | +| FPS | 1-30 (typically 25) | +| Data Option | **Single active (3)**, Speaker view (4), Gallery view (5), **Single individual stream** (March 2026) | + +**Rule**: Use JPG/PNG when fps <= 5, H.264 when fps > 5 + +### Video Configuration Example + +```javascript +const videoParams = { + codec: 7, // H.264 + resolution: 2, // HD 720p + fps: 25, + data_opt: 3 // Single active speaker +}; +``` + +### Single Individual Participant Video + +March 2026 added a new pattern for selecting **one participant camera stream at a time**. + +Use it when you need: + +- per-user vision processing +- a moderator-selected camera feed +- deterministic participant focus instead of active speaker switching + +Configuration rules: + +- set the video `data_opt` to `VIDEO_SINGLE_INDIVIDUAL_STREAM` +- subscribe to `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` +- send `VIDEO_SUBSCRIPTION_REQ` with the chosen `user_id` +- a new subscription overrides the previous participant stream + +This is not a multi-participant subscription feature. RTMS currently supports only **one** individual participant video stream at a time. + +### Processing Video + +```javascript +RTMSManager.on('video', ({ buffer, userName, timestamp }) => { + // buffer = H.264 NAL units + // Decode with FFmpeg, save, or stream + videoDecoder.decode(buffer); +}); +``` + +## Screen Share (SEPARATE from Video!) + +Screen share has a **different event** from regular video (msg_type 16 vs 15). + +| Property | Options | +|----------|---------| +| Codec | **JPG (5)**, PNG (6), H.264 (7) | +| Resolution | SD (1), **HD 720p (2)**, FHD 1080p (3), QHD 2K (4) | +| FPS | **1-5** for static content, 15-30 for animations | + +### Screen Share Configuration + +```javascript +const deskshareParams = { + codec: 5, // JPG (good for static slides) + resolution: 2, // HD + fps: 1 // Low FPS for slides +}; +``` + +### Processing Screen Share + +```javascript +RTMSManager.on('sharescreen', ({ buffer, userName, timestamp }) => { + // buffer = JPG/PNG image or H.264 frame + saveScreenCapture(buffer); +}); +``` + +## Transcript + +| Property | Value | +|----------|-------| +| Format | JSON text | +| Content Type | 5 (MEDIA_CONTENT_TYPE_TEXT) | +| Languages | 36 supported (see below) | +| `src_language` | Fixed requested language | +| `enable_lid` | Toggle Language Identification (default enabled) | + +### Language IDs (Common) + +| Language | ID | +|----------|-----| +| English | 9 | +| Chinese (Simplified) | 4 | +| Chinese (Traditional) | 5 | +| Japanese | 20 | +| Korean | 21 | +| Spanish | 28 | +| French (France) | 13 | +| German | 14 | + +**Tip**: Use `src_language` plus `enable_lid: false` to force a fixed language. Leave `enable_lid` enabled when you want automatic language switching. + +### Transcript Structure + +```json +{ + "user_id": "user_id", + "user_name": "Speaker Name", + "text": "Transcribed text content", + "timestamp": 1234567890, + "is_final": true +} +``` + +### Processing Transcript + +```javascript +RTMSManager.on('transcript', ({ text, userName, timestamp }) => { + // text = transcribed speech + // is_final = true for finalized segments + saveTranscript(userName, text); +}); +``` + +## Chat + +| Property | Value | +|----------|-------| +| Format | JSON text | +| Content Type | 5 (MEDIA_CONTENT_TYPE_TEXT) | + +### Processing Chat + +```javascript +RTMSManager.on('chat', ({ text, userName, timestamp }) => { + console.log(`[Chat] ${userName}: ${text}`); + saveChatMessage(userName, text); +}); +``` + +## Complete Media Configuration + +```javascript +const mediaParams = { + audio: { + content_type: 1, // RTP + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 + }, + video: { + codec: 7, // H.264 + resolution: 2, // HD 720p + fps: 25, + data_opt: 3 // Single active speaker + }, + deskshare: { + codec: 5, // JPG + resolution: 2, // HD + fps: 1 + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false + }, + chat: { + content_type: 5 // TEXT + } +}; +``` + +## Resources + +- **Data types**: https://developers.zoom.us/docs/rtms/data-types/ +- **Media params**: https://developers.zoom.us/docs/rtms/media-parameter-definition/ +- **MEDIA_PARAMETERS.md**: https://github.com/zoom/rtms-samples/blob/main/MEDIA_PARAMETERS.md diff --git a/partner-built/zoom-plugin/skills/rtms/references/quickstart.md b/partner-built/zoom-plugin/skills/rtms/references/quickstart.md new file mode 100644 index 00000000..830d6b3c --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/quickstart.md @@ -0,0 +1,215 @@ +# RTMS - Quickstart + +Get started with Zoom Realtime Media Streams. + +## Prerequisites + +1. **Node.js 20.3.0+** (24 LTS recommended) +2. Zoom General App (for meetings/webinars), Video SDK App (for Video SDK), or approved Contact Center / RTMS integration for Zoom Contact Center Voice +3. Webhook endpoint configured +4. Server to handle WebSocket connections + +## Environment Setup + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id # From App Credentials +ZOOM_CLIENT_SECRET=your_client_secret # From App Credentials +ZOOM_SECRET_TOKEN=your_webhook_token # From Feature → Webhook +``` + +## Option 1: RTMSManager (Recommended) + +Clone the official samples and use RTMSManager: + +```bash +git clone https://github.com/zoom/rtms-samples.git +cd rtms-samples/boilerplate/working_js +npm install +``` + +```javascript +import { RTMSManager } from './library/javascript/rtmsManager/RTMSManager.js'; +import express from 'express'; +import crypto from 'crypto'; + +const app = express(); +app.use(express.json()); + +// Initialize with credentials +await RTMSManager.init({ + credentials: { + meeting: { + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + secretToken: process.env.ZOOM_SECRET_TOKEN, + } + }, + // Use bitmask: AUDIO(1) | TRANSCRIPT(8) = 9 + mediaTypes: RTMSManager.MEDIA.AUDIO | RTMSManager.MEDIA.TRANSCRIPT, + logging: 'info' +}); + +// Handle media events +RTMSManager.on('audio', ({ buffer, userName }) => { + console.log(`Audio from ${userName}: ${buffer.length} bytes`); +}); + +RTMSManager.on('transcript', ({ text, userName }) => { + console.log(`${userName}: ${text}`); +}); + +RTMSManager.on('chat', ({ text, userName }) => { + console.log(`[Chat] ${userName}: ${text}`); +}); + +RTMSManager.on('sharescreen', ({ buffer, userName }) => { + console.log(`Screen share from ${userName}`); +}); + +// CRITICAL: Respond 200 IMMEDIATELY before any processing! +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + const { event, payload } = req.body; + + // Handle URL validation challenge + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + return res.json({ plainToken: payload.plainToken, encryptedToken: hash }); + } + + // Feed RTMS events to manager + RTMSManager.handleEvent(event, payload); +}); + +await RTMSManager.start(); +app.listen(3000, () => console.log('RTMS server on port 3000')); +``` + +## Option 2: @zoom/rtms SDK + +```bash +npm install @zoom/rtms express +``` + +```javascript +import rtms from "@zoom/rtms"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onAudioData((data, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}`); + }); + + client.onTranscriptData((data, timestamp, metadata) => { + console.log(`${metadata.userName}: ${data}`); + }); + + client.onChatData((data, timestamp, metadata) => { + console.log(`[Chat] ${metadata.userName}: ${data}`); + }); + + client.onScreenShareData((data, timestamp, metadata) => { + console.log(`Screen share from ${metadata.userName}`); + }); + + // SDK accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Zoom App Setup Steps + +### For Meetings and Webinars (General App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) +2. Click **Develop** → **Build App** +3. Choose **General App** → **User-Managed** +4. Features → Access → **Enable Event Subscription** +5. Add Events → Search "rtms" → Select RTMS endpoints: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +6. Scopes → Add Scopes → Search "rtms" → Add: + - `meeting:read:meeting_audio` + - `meeting:read:meeting_video` + - `meeting:read:meeting_transcript` + - `meeting:read:meeting_chat` + - `webinar:read:webinar_audio` (if using webinars) + - `webinar:read:webinar_video` (if using webinars) + - `webinar:read:webinar_transcript` (if using webinars) + - `webinar:read:webinar_chat` (if using webinars) + +### For Video SDK (Video SDK App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) +2. Click **Develop** → **Build App** +3. Choose **Video SDK App** +4. Add Events: + - `session.rtms_started` + - `session.rtms_stopped` +5. Use SDK Key and SDK Secret for authentication (not OAuth credentials) + +## How to Start RTMS + +RTMS must be started for each meeting/webinar/session. Options: + +| Product | How to Start | Webhook Event | +|---------|--------------|---------------| +| Meeting | Zoom client, REST API, Zoom App SDK, or **autostart** (zoom.us settings) | `meeting.rtms_started` | +| Webinar | Zoom client, REST API, Zoom App SDK, or **autostart** (zoom.us settings) | `webinar.rtms_started` | +| Video SDK | Video SDK client or REST API | `session.rtms_started` | +| Zoom Contact Center Voice | Product-specific RTMS/ZCC Voice flow | Product-specific RTMS/ZCC Voice events | + +> **Webinar note**: Panelists have full audio/video streams. Attendee streams may not be available individually. + +> **March 2026 note**: transcript handshakes now support `src_language` plus `enable_lid`, media socket keep-alive tolerance is now about **65s**, and RTMS supports one selected participant camera stream at a time via `VIDEO_SINGLE_INDIVIDUAL_STREAM` + `VIDEO_SUBSCRIPTION_REQ`. + +## Deployment with Reverse Proxy + +When deploying behind nginx with a path prefix (e.g., `/my-app/`): + +1. **Socket.IO path must match nginx config**: +```javascript +const socketPath = window.location.pathname.includes('/my-app') + ? '/my-app/rtms-socket' + : '/rtms-socket'; +const socket = io({ path: socketPath }); +``` + +2. **API calls must use relative paths**: +```javascript +const basePath = window.location.pathname.replace(/\/$/, ''); +fetch(`${basePath}/api/sessions`); // NOT fetch('/api/sessions') +``` + +3. **Nginx WebSocket proxy**: +```nginx +location /my-app/rtms-socket { + proxy_pass http://YOUR_RTMS_BACKEND_HOST:3000/rtms-socket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +} +``` + +## Next Steps + +- [Media Types](media-types.md) - All 5 data types (audio, video, transcript, chat, screen share) +- [Connection](connection.md) - WebSocket protocol & message types +- [Webhooks](webhooks.md) - Event subscription details + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **rtms-samples**: https://github.com/zoom/rtms-samples diff --git a/partner-built/zoom-plugin/skills/rtms/references/webhooks.md b/partner-built/zoom-plugin/skills/rtms/references/webhooks.md new file mode 100644 index 00000000..b3babf84 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/references/webhooks.md @@ -0,0 +1,275 @@ +# RTMS - Webhooks + +RTMS-related webhook events and configuration. + +## CRITICAL: Respond 200 IMMEDIATELY! + +**The #1 cause of random disconnects:** + +If your webhook handler takes too long to respond, Zoom assumes failure and retries. The retry creates a second connection, which kicks out your first connection (only 1 connection allowed per stream). + +```javascript +// CORRECT: Respond first, process async +app.post('/webhook', (req, res) => { + res.status(200).send(); // IMMEDIATELY! + + // Then process asynchronously + handleRTMSEvent(req.body); +}); + +// WRONG: Processing before responding +app.post('/webhook', async (req, res) => { + await heavyProcessing(req.body); // Zoom may retry while waiting! + res.status(200).send(); +}); +``` + +## URL Validation Challenge + +When configuring your webhook URL, Zoom sends a validation challenge: + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + // Handle URL validation + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hash + }); + } + + res.status(200).send(); + // ... handle other events +}); +``` + +## Events + +### meeting.rtms_started + +Sent when RTMS stream is ready for a meeting. + +```json +{ + "event": "meeting.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "meeting_uuid": "meeting_uuid", + "host_id": "host_user_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +### meeting.rtms_stopped + +Sent when RTMS stream ends. + +```json +{ + "event": "meeting.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### webinar.rtms_started + +Sent when RTMS stream is ready for a webinar. + +```json +{ + "event": "webinar.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "meeting_uuid": "meeting_uuid", + "host_id": "host_user_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +> **Important**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`. The signature and connection flow are identical to meetings. + +**Webinar-specific considerations:** +- **Panelists**: Full audio/video streams are available for panelists. +- **Attendees**: View-only participants; individual streams may not be available. +- **Practice sessions**: Not documented for RTMS. +- **Q&A/Polls**: Not exposed via RTMS. + +### webinar.rtms_stopped + +Sent when RTMS stream ends for a webinar. + +```json +{ + "event": "webinar.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### session.rtms_started + +Sent when RTMS stream is ready for a Video SDK session. + +```json +{ + "event": "session.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "session_id": "session_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +> **Important**: Video SDK payloads use `session_id` instead of `meeting_uuid`. The HMAC signature must use `session_id` in place of `meeting_uuid`. + +**Video SDK-specific considerations:** +- Uses **SDK Key/Secret** (not OAuth Client ID/Secret) for authentication. +- Requires a **Video SDK App** (not a General App) in Zoom Marketplace. +- Once connected, the WebSocket protocol is identical to meetings. + +### session.rtms_stopped + +Sent when RTMS stream ends for a Video SDK session. + +```json +{ + "event": "session.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "session_id": "session_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### Screen Share Events (via msg_type 5) + +Subscribe to receive `SHARING_START` and `SHARING_STOP` events when participants start/stop screen sharing. + +## Payload Fields + +| Field | Description | +|-------|-------------| +| `rtms_stream_id` | Unique stream identifier | +| `server_urls` | WebSocket signaling server URL | +| `meeting_uuid` | Meeting unique identifier (needed for signature) | +| `signature` | Pre-computed auth signature (alternative to self-generating) | + +## Server URL Geo-Routing + +Server URLs contain airport/region codes: + +| Code | Location | +|------|----------| +| `sjc` | San Jose, California | +| `iad` | Washington DC | +| `sin` | Singapore | +| `fra` | Frankfurt, Germany | +| `syd` | Sydney, Australia | + +```javascript +// Extract region from server URL +const hostname = new URL(serverUrl).hostname; // rtms-sjc1.zoom.us +const region = hostname.split('-')[1].replace(/[0-9]/g, ''); // sjc +``` + +**Tip**: For production, route webhooks to workers in the same region as the Zoom server. + +## Subscribing to RTMS Events + +### In Zoom Marketplace (General App - Meetings and Webinars) + +1. Go to your app settings +2. Navigate to **Features** → **Access** +3. **Enable Event Subscription** +4. Click **Add Event Subscription** +5. Enter your webhook endpoint URL +6. Search "rtms" and select: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +7. Click **Done** then **Save** + +### In Zoom Marketplace (Video SDK App) + +1. Go to your Video SDK app settings +2. Add Event Subscription: + - `session.rtms_started` + - `session.rtms_stopped` + +### Required Scopes + +**For Meetings** (Features → Scopes → Add Scopes → search "rtms"): + +| Scope | Purpose | +|-------|---------| +| `meeting:read:meeting_audio` | Access meeting audio | +| `meeting:read:meeting_video` | Access meeting video | +| `meeting:read:meeting_transcript` | Access transcripts | +| `meeting:read:meeting_chat` | Access chat messages | + +**For Webinars** (add these in addition to meeting scopes): + +| Scope | Purpose | +|-------|---------| +| `webinar:read:webinar_audio` | Access webinar audio | +| `webinar:read:webinar_video` | Access webinar video | +| `webinar:read:webinar_transcript` | Access webinar transcripts | +| `webinar:read:webinar_chat` | Access webinar chat messages | + +**For Video SDK**: Uses SDK Key/Secret credentials instead of OAuth scopes. + +## Products Supporting RTMS + +| Product | Start Event | Stop Event | Payload ID | App Type | +|---------|-------------|------------|------------|----------| +| **Zoom Meetings** | `meeting.rtms_started` | `meeting.rtms_stopped` | `meeting_uuid` | General App | +| **Zoom Webinars** | `webinar.rtms_started` | `webinar.rtms_stopped` | `meeting_uuid` (not webinar_uuid!) | General App | +| **Zoom Video SDK** | `session.rtms_started` | `session.rtms_stopped` | `session_id` | Video SDK App | +| Zoom Contact Center | `contactcenter.rtms_*` | `contactcenter.rtms_*` | See Zoom docs | Contact Center App | +| Zoom Phone | `phone.rtms_*` | `phone.rtms_*` | See Zoom docs | General App | + +> **Key differences**: Meetings and webinars use a General App with OAuth credentials. Video SDK uses a Video SDK App with SDK Key/Secret. Once connected, the WebSocket protocol is identical across all products. + +## Resources + +- **Event reference**: https://developers.zoom.us/docs/rtms/event-reference/ +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ diff --git a/partner-built/zoom-plugin/skills/rtms/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/rtms/troubleshooting/common-issues.md new file mode 100644 index 00000000..d5246857 --- /dev/null +++ b/partner-built/zoom-plugin/skills/rtms/troubleshooting/common-issues.md @@ -0,0 +1,404 @@ +# Common Issues + +Troubleshooting guide for Zoom RTMS. + +## Quick Diagnostics + +| Symptom | Likely Cause | Solution | +|---------|--------------|----------| +| Connection fails | Invalid signature | Check signature generation | +| Duplicate connections | Slow webhook response | Respond 200 immediately | +| No data received | Wrong media type | Check media_type bitmask | +| Connection closes | Missing heartbeat | Respond to msg_type 12 | +| Segmentation fault | Old Node.js | Upgrade to 20.3.0+ | + +## Connection Issues + +### Webhook Response Timing + +**Problem**: Random disconnections, duplicate connections + +**Cause**: If your webhook handler takes too long to respond, Zoom retries. The retry creates a second connection, which kicks out the first (only 1 connection allowed per stream). + +**Solution**: Respond 200 IMMEDIATELY before any processing: + +```javascript +// CORRECT +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + // Then process asynchronously + setImmediate(() => { + handleRTMSEvent(req.body); + }); +}); + +// WRONG +app.post('/webhook', async (req, res) => { + await heavyProcessing(req.body); // Zoom retries while waiting! + res.status(200).send(); +}); +``` + +### Duplicate Connection Prevention + +**Problem**: Multiple connections to same stream + +**Solution**: Track active sessions: + +```javascript +const activeSessions = new Map(); + +function handleRTMSStarted(payload) { + const streamId = payload.rtms_stream_id; + + if (activeSessions.has(streamId)) { + console.log('Already connected, ignoring duplicate'); + return; + } + + activeSessions.set(streamId, Date.now()); + connectToRTMS(payload); +} + +function handleRTMSStopped(payload) { + activeSessions.delete(payload.rtms_stream_id); +} +``` + +### Invalid Signature + +**Problem**: Handshake fails with status_code 3 + +**Cause**: Signature generation incorrect + +**Solution**: Verify format: + +```javascript +// Message format: "clientId,meetingUuid,streamId" +const message = `${clientId},${meetingUuid},${streamId}`; +const signature = crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +``` + +**Checklist**: +- [ ] Using correct clientId (not app name) +- [ ] Using correct clientSecret +- [ ] No extra spaces in message +- [ ] Using hex output (not base64) + +### Connection Timeout + +**Problem**: WebSocket connection times out + +**Causes**: +- Network issues +- Firewall blocking WebSocket +- Server URL expired + +**Solution**: +1. Check network connectivity +2. Ensure firewall allows WSS +3. Use fresh webhook payload (don't cache URLs) + +## Heartbeat Issues + +### Connection Closes Unexpectedly + +**Problem**: Connection closes after ~60 seconds + +**Cause**: Not responding to heartbeat + +**Solution**: Respond to msg_type 12 with msg_type 13: + +```javascript +ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 12) { + ws.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + } +}); +``` + +**Timeouts**: +- Signaling: ~60 seconds +- Media: ~65 seconds + +## Media Data Issues + +### No Audio Data + +**Causes**: +1. Wrong media_type in handshake +2. No participants speaking +3. Audio not enabled in meeting + +**Solution**: +1. Verify media_type includes AUDIO (1): + ```javascript + media_type: 1 // Just audio + media_type: 9 // Audio + Transcript + media_type: 32 // All media + ``` +2. Wait for participant to speak +3. Check meeting audio settings + +### No Video Data + +**Causes**: +1. Wrong media_type +2. No video enabled +3. Wrong codec for FPS +4. Individual video mode enabled but no participant subscription sent + +**Solution**: +1. Include VIDEO (2) in media_type +2. Use H.264 for fps > 5: + ```javascript + video: { + codec: 7, // H.264 + resolution: 2, // HD + fps: 25 // > 5 requires H.264 + } + ``` +3. If using `VIDEO_SINGLE_INDIVIDUAL_STREAM`, also: + - subscribe to `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` + - send `VIDEO_SUBSCRIPTION_REQ` with a live `user_id` + - remember a new request replaces the previous participant stream + +### No Screen Share Data + +**Problem**: Not receiving screen share even when active + +**Cause**: Screen share is SEPARATE from video + +**Solution**: Include DESKSHARE (4) in media_type: +```javascript +media_type: 4 // Just screen share +media_type: 5 // Audio + screen share +media_type: 32 // All media +``` + +### Transcript Language Delay + +**Problem**: noticeable startup delay before transcription stabilizes + +**Cause**: Language Identification (LID) is enabled and RTMS is auto-detecting / auto-switching languages + +**Solution**: Set a source language and disable LID when you want a fixed language: +```javascript +transcript: { + content_type: 5, + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch +} +``` + +**Language IDs**: +| ID | Language | +|----|----------| +| 9 | English | +| 4 | Chinese (Simplified) | +| 20 | Japanese | +| 21 | Korean | +| 28 | Spanish | + +See [Data Types](../references/data-types.md#transcript-languages) for full list. + +### Participant Video Events Arrive But No Video Stream Follows + +**Problem**: You receive `PARTICIPANT_VIDEO_ON`, but no actual participant video frames arrive. + +**Cause**: Those events only tell you whose camera is currently available. They do not automatically switch the data socket to that participant. + +**Solution**: + +1. open the video media socket with `VIDEO_SINGLE_INDIVIDUAL_STREAM` +2. handle `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` +3. choose one `user_id` +4. send `VIDEO_SUBSCRIPTION_REQ` +5. wait for `VIDEO_SUBSCRIPTION_RESP` + +Also remember: + +- only one participant stream is supported at a time +- a newer subscription overrides the previous participant stream + +### Stream Never Closes Cleanly From Backend + +**Problem**: Your app finishes processing, but the RTMS stream remains open until external stop events arrive. + +**Solution**: Use the new graceful-close control message on the signaling socket: + +```javascript +signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId +})); +``` + +Treat `STREAM_CLOSE_RESP` as acknowledgement, then continue with local cleanup. + +## SDK-Specific Issues + +### Segmentation Fault + +**Problem**: App crashes with segmentation fault + +**Cause**: Node.js version < 20.3.0 + +**Solution**: +```bash +# Check version +node --version + +# Upgrade with nvm +nvm install 24 +nvm use 24 + +# Clear cache and reinstall +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +``` + +### Audio Metadata Missing userId + +**Problem**: `onAudioData` metadata doesn't include speaker userId + +**Cause**: Using AUDIO_MIXED_STREAM (all audio combined) + +**Solution**: Use `onActiveSpeakerEvent` for speaker identification: +```javascript +client.onActiveSpeakerEvent((timestamp, userId, userName) => { + console.log(`Current speaker: ${userName}`); +}); +``` + +Or use AUDIO_MULTI_STREAMS: +```javascript +client.setAudioParams({ + dataOpt: 2 // Per-participant streams +}); +``` + +### Video Parameters Ignored + +**Problem**: `setVideoParams` not taking effect + +**Cause**: SDK bug - video params ignored after audio params + +**Workaround**: Call `setVideoParams` BEFORE `setAudioParams`: +```javascript +// CORRECT ORDER +client.setVideoParams({ codec: 7, fps: 25 }); +client.setAudioParams({ codec: 4, sampleRate: 3 }); +client.join(payload); +``` + +### SDK Invalid State + +**Problem**: "Invalid status" error on join + +**Cause**: SDK still cleaning up from previous session + +**Solution**: Retry with delay: +```javascript +try { + client.join(payload); +} catch (error) { + if (error.message?.includes('Invalid status')) { + console.warn('SDK cleaning up, retrying in 2s'); + + setTimeout(() => { + client.join(payload); + }, 2000); + } +} +``` + +## Platform Issues + +### Platform Not Supported + +**Problem**: SDK installation fails + +**Currently Supported**: +- darwin-arm64 (Apple Silicon) +- linux-x64 + +**Not Yet Supported**: +- Windows +- darwin-x64 (Intel Mac) +- linux-arm64 + +**Workaround**: Use [Manual WebSocket](../examples/manual-websocket.md) implementation. + +## Status Code Reference + +| Code | Name | Description | +|------|------|-------------| +| 0 | STATUS_OK | Success | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Already connected (signaling) | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Already connected (media) | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +See [Data Types](../references/data-types.md) for complete list. + +## Product-Specific Issues + +### Video SDK Issues + +**Problem**: Signature validation fails for Video SDK sessions + +**Cause**: Using OAuth Client ID/Secret instead of SDK Key/Secret, or using `meeting_uuid` instead of `session_id`. + +**Solution**: +- Video SDK apps use **SDK Key** (as `clientId`) and **SDK Secret** (as `clientSecret`). +- Video SDK webhook payloads contain `session_id`, NOT `meeting_uuid`. +- The HMAC signature must use `session_id`: `HMAC-SHA256(sdkSecret, "sdkKey,sessionId,streamId")` + +```javascript +// Extract the correct ID based on product +const idValue = payload.meeting_uuid || payload.session_id; +const signature = generateSignature(clientId, idValue, streamId, clientSecret); +``` + +### Webinar Issues + +**Problem**: Looking for `webinar_uuid` in the payload + +**Cause**: Expecting a webinar-specific UUID field. + +**Solution**: Webinar RTMS payloads still use `meeting_uuid` (NOT `webinar_uuid`). This is a common gotcha. The signature, connection flow, and protocol are identical to meetings. + +**Problem**: Missing attendee streams in webinars + +**Cause**: Webinar attendees are view-only participants. + +**Solution**: Only **panelist** audio/video streams are confirmed to be available via RTMS. Attendee streams may not be available individually. Design your application to work with panelist streams only. + +**Problem**: Practice session not triggering RTMS + +**Cause**: Practice sessions are not documented for RTMS support. + +**Solution**: RTMS events are expected when the webinar goes live to attendees, not during practice sessions. Q&A and Polls data are also not exposed via RTMS. + +## Getting Help + +1. **Developer Forum**: https://devforum.zoom.us/ +2. **GitHub Issues**: https://github.com/zoom/rtms/issues +3. **Official Docs**: https://developers.zoom.us/docs/rtms/ + +## Next Steps + +- **[Connection Architecture](../concepts/connection-architecture.md)** - Understand the protocol +- **[Lifecycle Flow](../concepts/lifecycle-flow.md)** - Correct connection sequence +- **[Data Types](../references/data-types.md)** - All status codes and enums diff --git a/partner-built/zoom-plugin/skills/scribe/RUNBOOK.md b/partner-built/zoom-plugin/skills/scribe/RUNBOOK.md new file mode 100644 index 00000000..bb9dc2c6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/RUNBOOK.md @@ -0,0 +1,88 @@ +# Scribe 5-Minute Preflight Runbook + +Use this before deep debugging. + +## 1) Confirm the Right Product + +- File-based or storage-based transcription -> stay on `scribe`. +- Live meeting media stream or botless live transcription -> use `rtms` instead. +- Meeting bot that joins and records before transcription -> chain Meeting SDK Linux first. + +## 2) Confirm Credentials + +- Build-platform issuer credential pair available. +- JWT generation uses `HS256` with one-hour-or-less expiry. +- Secret stays server-side. +- Reject placeholder values such as `${ZOOM_API_KEY}` and `${ZOOM_API_SECRET}`. They can make a naive health check look configured while every real call still fails. + +## 3) Confirm Mode Selection + +- **Fast mode** for one short file and immediate JSON response. +- **Batch mode** for many files, long recordings, or archive-style processing. +- **Browser microphone pseudo-streaming** for short repeated chunks uploaded through the async fast-mode wrapper. +- Fast mode current limits from the API spec: + - maximum file size: `100 MB` + - maximum duration: `2 hours` +- If fast mode is exposed through a hosted browser UI, prefer an async wrapper: + - browser uploads once + - backend returns `202` with a request ID + - frontend polls for completion + This avoids losing successful transcriptions to edge/client timeout races. +- Observed hosted timing from the deployed sample: + - ~17.2 MB MP4 completed in ~26s + - ~38.6 MB MP4 completed in ~26-37s + - ~59.2 MB MP4 completed in ~32-34s on the backend + - some ~59.2 MB requests still surfaced as frontend `504` even though the backend later completed with `200` + Treat these as deployment observations, not hard API guarantees. +- Recommended starting browser mic cadence: + - chunk size: `5 seconds` + - acceptable range: `5-10 seconds` + - keep only `2-3` chunks in flight at once + This gives incremental transcript updates without trying to hold a single long browser request open. +- For browser mic capture, rotate the recorder per chunk so each uploaded blob is a standalone file. + Do not assume `MediaRecorder.start(timeslice)` later chunks will always be independently transcribable. +- Do not treat this as the default production solution for live transcription. + Prefer `rtms` when the user actually needs a live-audio product instead of a browser demo. + +## 4) Confirm Storage / Webhook Inputs + +- Fast mode file URL or upload path resolves. +- Batch input/output URIs are valid. +- AWS or pre-signed access is set correctly for S3 mode. +- Webhook URL is public HTTPS if you expect notifications. + +## 5) Confirm Post-Processing Contract + +- Decide whether downstream code expects `text_display`, segments, or word-level timings. +- Decide whether channel separation or diarization is required before shipping. + +## 6) Quick Probes + +- JWT generation works locally. +- `POST /aiservices/scribe/transcribe` succeeds with a known small file. +- For browser-uploaded files, backend forwarding should use `multipart/form-data` to Zoom, not a JSON `data:` URI wrapper. +- Batch submit returns `201` with `job_id`. +- Webhook signature verification works with the configured secret. + +## 7) Fast Decision Tree + +- `401`/auth failure -> wrong credential pair or expired JWT. +- Fast mode returns schema error -> wrong request body or config fields. +- Fast mode returns `413 Request Entity Too Large` before the app logs anything -> reverse proxy limit, not Scribe. +- Frontend returns `504` but backend logs later show `200` -> browser/edge timeout race; poll by request ID instead of assuming failure. +- Browser mic feature needs true continuous low-latency media instead of chunked uploads -> switch to `rtms`, not `scribe`. +- Browser mic chunk 1 works but chunk 2 onward is empty -> recorder/container boundary issue; restart the recorder for each chunk. +- Batch jobs queue but never complete -> storage auth / URI / webhook issues. +- Missing transcripts for some files -> inspect `/jobs/{jobId}/files` before re-submitting whole batch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/ai-services/ +- https://developers.zoom.us/docs/ai-services/scribe/ +- https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json + +### Raw docs in repo tooling output + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/ai-services/` diff --git a/partner-built/zoom-plugin/skills/scribe/SKILL.md b/partner-built/zoom-plugin/skills/scribe/SKILL.md new file mode 100644 index 00000000..8320a4ca --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/SKILL.md @@ -0,0 +1,117 @@ +--- +name: scribe +description: "Reference skill for Zoom AI Services Scribe. Use after routing to a transcription workflow when handling uploaded or stored media, Build-platform JWT auth, fast mode transcription, batch jobs, or transcript pipeline design." +user-invocable: false +triggers: + - "scribe" + - "ai services scribe" + - "zoom scribe" + - "transcribe audio file" + - "transcribe video file" + - "batch transcription" + - "fast mode transcription" + - "build platform jwt" +--- + +# Zoom AI Services Scribe + +Background reference for Zoom AI Services Scribe across: +- synchronous single-file transcription (`POST /aiservices/scribe/transcribe`) +- asynchronous batch jobs (`/aiservices/scribe/jobs*`) +- browser microphone pseudo-streaming via repeated short file uploads +- webhook-driven batch status updates +- Build-platform JWT generation and credential handling + +Official docs: +- https://developers.zoom.us/docs/ai-services/ +- https://developers.zoom.us/docs/ai-services/scribe/ +- https://developers.zoom.us/docs/api/ai-services/ +- https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json +- Quickstart sample: https://github.com/zoom/scribe-quickstart/ + +## Routing Guardrail + +- If the user needs **uploaded or stored media transcribed into text**, route here first. +- If the user needs **live meeting media** without file-based upload/batch jobs, route to [../rtms/SKILL.md](../rtms/SKILL.md). +- If the user needs **Zoom REST API inventory** for AI Services paths, chain [../rest-api/SKILL.md](../rest-api/SKILL.md). +- If the user needs webhook signature patterns or generic HMAC receiver hardening, optionally chain [../webhooks/SKILL.md](../webhooks/SKILL.md). + +## Quick Links + +1. [concepts/auth-and-processing-modes.md](concepts/auth-and-processing-modes.md) +2. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +3. [examples/fast-mode-node.md](examples/fast-mode-node.md) +4. [examples/batch-webhook-pipeline.md](examples/batch-webhook-pipeline.md) +5. [references/api-reference.md](references/api-reference.md) +6. [references/environment-variables.md](references/environment-variables.md) +7. [references/samples-validation.md](references/samples-validation.md) +8. [references/versioning-and-drift.md](references/versioning-and-drift.md) +9. [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) +10. [RUNBOOK.md](RUNBOOK.md) + +## Core Workflow + +1. Get Build-platform credentials and generate an HS256 JWT. +2. Choose **fast mode** for one short file or **batch mode** for stored archives / large sets. +3. Submit the transcription request. +4. For batch jobs, poll job/file status or receive webhook notifications. +5. Persist and post-process transcript JSON. + +## Hosted Fast-Mode Guardrail + +- The formal fast-mode API limits are `100 MB` and `2 hours`, but hosted browser flows can still time out before the upstream response returns. +- Current deployed-sample observations: + - ~17.2 MB MP4 completed in about `26s` + - ~38.6 MB MP4 completed in about `26-37s` + - ~59.2 MB MP4 completed in about `32-34s` on the backend + - some ~59.2 MB browser requests still surfaced as frontend `504` while backend logs later showed `200` +- Treat frontend `504` plus backend `200` as a browser/edge timeout race, not an automatic transcription failure. +- For hosted UIs, prefer an async request/polling wrapper for fast mode instead of holding the browser open for the full upstream response. +- For larger or less predictable media, prefer batch mode even when the file is still within the formal fast-mode size limit. + +## Browser Microphone Pattern + +- `scribe` does not expose a documented real-time streaming API surface. +- If you want a browser microphone experience, use pseudo-streaming: + 1. capture microphone audio in short chunks + 2. upload each chunk through the async fast-mode wrapper + 3. poll for completion + 4. append chunk transcripts in sequence +- Recommended starting cadence: + - chunk size: `5 seconds` + - acceptable range: `5-10 seconds` + - in-flight chunk requests: `2-3` +- This is a practical UI pattern for incremental transcript updates, not a substitute for `rtms`. +- Treat this as a fallback demo pattern, not the preferred production architecture. +- It adds repeated upload overhead, chunk-boundary drift, browser codec/container variability, and transcript stitching complexity. +- If the user asks for actual live stream ingestion, low-latency continuous media, or server-push media transport, route to [../rtms/SKILL.md](../rtms/SKILL.md) instead. + +## Endpoint Surface + +| Mode | Method | Path | Use | +|------|--------|------|-----| +| Fast | `POST` | `/aiservices/scribe/transcribe` | Synchronous transcription for one file | +| Batch | `POST` | `/aiservices/scribe/jobs` | Submit asynchronous batch job | +| Batch | `GET` | `/aiservices/scribe/jobs` | List jobs | +| Batch | `GET` | `/aiservices/scribe/jobs/{jobId}` | Inspect job summary/state | +| Batch | `DELETE` | `/aiservices/scribe/jobs/{jobId}` | Cancel queued/processing job | +| Batch | `GET` | `/aiservices/scribe/jobs/{jobId}/files` | Inspect per-file results | + +## High-Level Scenarios + +- On-demand clip transcription after a user uploads one recording. +- Batch transcription of stored S3 call archives. +- Webhook-driven ETL pipeline that writes transcripts to your database/search index. +- Re-transcription of Zoom-managed recordings after exporting them to your own storage. +- Offline compliance or QA workflows that need timestamps, channel separation, and speaker hints. + +## Chaining + +- Stored Zoom recordings -> [../rest-api/SKILL.md](../rest-api/SKILL.md) + `scribe` +- Webhook verification hardening -> [../webhooks/SKILL.md](../webhooks/SKILL.md) +- Real-time live transcript/media -> [../rtms/SKILL.md](../rtms/SKILL.md) +- Cross-product routing -> [../general/SKILL.md](../general/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/scribe/concepts/auth-and-processing-modes.md b/partner-built/zoom-plugin/skills/scribe/concepts/auth-and-processing-modes.md new file mode 100644 index 00000000..55ac0a1b --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/concepts/auth-and-processing-modes.md @@ -0,0 +1,95 @@ +# Auth and Processing Modes + +## Authentication Model + +Scribe uses a Build-platform JWT bearer token. + +JWT shape: +- algorithm: `HS256` +- issuer claim: Build-platform credential identifier used by the Scribe API +- expiration: keep to one hour or less + +Node example: + +```js +import { KJUR } from 'jsrsasign'; + +export function generateJWT(apiKey, apiSecret) { + const iat = Math.round(Date.now() / 1000) - 30; + const exp = iat + 60 * 60; + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify({ iss: apiKey, iat, exp }), + apiSecret, + ); +} +``` + +## Credential Naming Drift + +Zoom docs currently use inconsistent labels across AI Services pages: +- `API key` / `API secret` +- `SDK key` / `SDK secret` +- `Build platform credentials` + +For implementation, treat them as the Build-platform JWT issuer/secret pair used to sign Scribe requests. Verify the exact labels in the current portal UI before shipping. + +## Fast Mode vs Batch Mode + +| Mode | Best for | Transport | Result timing | +|------|----------|-----------|---------------| +| Fast mode | One short file, interactive UX | `POST /transcribe` | Immediate synchronous JSON | +| Batch mode | Archives, long media, many files | `POST /jobs` then status/webhook | Asynchronous | + +## Fast Mode Request Shape + +- required: `file`, `config` +- common config: `language`, `word_time_offsets`, `channel_separation`, `timestamps`, `output_format`, `profanity_filter`, `diarization` + +## Batch Mode Request Shape + +- required: `input`, `output`, `config` +- input modes: `SINGLE`, `PREFIX`, `MANIFEST` +- storage provider currently surfaced in the OpenAPI as `S3` +- optional webhook callback: `notifications.webhook_url` + `notifications.secret` + +## Operational Choice + +Choose fast mode when: +- user uploads one file +- latency matters more than throughput +- file size and duration are manageable +- you are building pseudo-streaming over short microphone chunks from a browser UI + +Choose batch mode when: +- many files must be processed +- transcripts can arrive later +- storage-centric workflows fit better than direct upload + +## Browser Microphone Pseudo-Streaming + +Scribe is file-oriented, so a browser microphone UX should be modeled as repeated short uploads, not a long-lived stream. + +Recommended pattern: +1. capture browser microphone audio with `MediaRecorder` +2. flush short chunks to your backend +3. submit each chunk through the async fast-mode wrapper +4. poll by request ID +5. append transcript chunks in order + +Recommended starting values: +- chunk size: `5 seconds` +- acceptable range: `5-10 seconds` +- concurrent in-flight chunks: `2-3` + +Why this works: +- lowers the chance of frontend `504` on longer synchronous requests +- gives incremental transcript updates without waiting for one long request + +Guardrail: +- this is pseudo-streaming over file uploads +- this is not the preferred production design for live audio capture +- use it only when a lightweight browser demo or rough incremental transcript is acceptable +- avoid it when you need stable low-latency live transcription, lower overhead, or stronger continuity across utterances +- for true live media streams, low-latency server ingest, or continuous in-meeting audio, use `rtms` diff --git a/partner-built/zoom-plugin/skills/scribe/examples/batch-webhook-pipeline.md b/partner-built/zoom-plugin/skills/scribe/examples/batch-webhook-pipeline.md new file mode 100644 index 00000000..5716f777 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/examples/batch-webhook-pipeline.md @@ -0,0 +1,65 @@ +# Batch Job + Webhook Pipeline + +Use batch mode when you need to process stored archives asynchronously. + +## Flow + +```text +submit batch job + -> receive job_id + -> poll /jobs or wait for webhook + -> inspect /jobs/{jobId}/files + -> ingest transcript outputs +``` + +## Submit Example + +```bash +curl -X POST https://api.zoom.us/v2/aiservices/scribe/jobs -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{ + "input": { + "mode": "PREFIX", + "source": "S3", + "uri": "s3://example-bucket/audio/", + "auth": { + "aws": { + "access_key_id": "...", + "secret_access_key": "...", + "session_token": "..." + } + } + }, + "output": { + "destination": "S3", + "uri": "s3://example-bucket/transcripts/", + "layout": "PREFIX", + "auth": { + "aws": { + "access_key_id": "...", + "secret_access_key": "...", + "session_token": "..." + } + } + }, + "config": { + "language": "en-US", + "word_time_offsets": true, + "channel_separation": true + }, + "notifications": { + "webhook_url": "https://example.com/webhooks/scribe", + "secret": "replace-me" + } + }' +``` + +## Webhook Verification Pattern + +```js +import crypto from 'crypto'; + +function verifyZoomWebhook(rawBody, timestamp, signature, secret) { + const message = `v0:${timestamp}:${rawBody}`; + const expected = `sha256=${crypto.createHmac('sha256', secret).update(message).digest('hex')}`; + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); +} +``` diff --git a/partner-built/zoom-plugin/skills/scribe/examples/fast-mode-node.md b/partner-built/zoom-plugin/skills/scribe/examples/fast-mode-node.md new file mode 100644 index 00000000..5ceb10b6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/examples/fast-mode-node.md @@ -0,0 +1,64 @@ +# Fast Mode Node Example + +Minimal backend proxy for synchronous transcription. + +```js +import express from 'express'; +import multer from 'multer'; +import { KJUR } from 'jsrsasign'; + +const app = express(); +const upload = multer({ storage: multer.memoryStorage() }); +app.use(express.json()); + +function generateJWT() { + const iat = Math.round(Date.now() / 1000) - 30; + const exp = iat + 60 * 60; + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify({ iss: process.env.ZOOM_API_KEY, iat, exp }), + process.env.ZOOM_API_SECRET, + ); +} + +app.post('/transcribe', upload.single('file'), async (req, res) => { + const token = generateJWT(); + const config = { + language: req.body.language || 'en-US', + word_time_offsets: true, + channel_separation: false, + }; + + let response; + if (req.file) { + const form = new FormData(); + form.append('file', new Blob([new Uint8Array(req.file.buffer)]), req.file.originalname); + form.append('config', JSON.stringify(config)); + response = await fetch('https://api.zoom.us/v2/aiservices/scribe/transcribe', { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: form, + }); + } else { + response = await fetch('https://api.zoom.us/v2/aiservices/scribe/transcribe', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file: req.body.file, + config, + }), + }); + } + + const text = await response.text(); + res.status(response.status).type('application/json').send(text); +}); +``` + +Use this pattern when: +- the caller uploads a file to your backend and you forward it as multipart +- or the caller already has a URL-accessible media file and you submit the JSON URL form diff --git a/partner-built/zoom-plugin/skills/scribe/references/api-reference.md b/partner-built/zoom-plugin/skills/scribe/references/api-reference.md new file mode 100644 index 00000000..5b890e3c --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/references/api-reference.md @@ -0,0 +1,126 @@ +# Zoom AI Services Scribe API Reference + +Canonical sources: +- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json +- Docs overview: https://developers.zoom.us/docs/ai-services/scribe/ +- Base URL: `https://api.zoom.us/v2` + +## Endpoint Inventory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/aiservices/scribe/transcribe` | Scribe (Synchronous) | `createFastAsr` | +| POST | `/aiservices/scribe/jobs` | Submit Batch Scribe Job | `submitBatchAsr` | +| GET | `/aiservices/scribe/jobs` | List Batch Jobs | `listBatchJobs` | +| 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` | + +## Request Shapes + +### `POST /aiservices/scribe/transcribe` + +Required top-level fields: +- `file` +- `config` + +Common config fields: +- `language` +- `word_time_offsets` +- `channel_separation` +- `timestamps` +- `output_format` +- `profanity_filter` +- `diarization` + +Response keys: +- `request_id` +- `duration_sec` +- `model` +- `result` + +### `POST /aiservices/scribe/jobs` + +Required top-level fields: +- `input` +- `output` +- `config` + +Input subfields: +- `mode` (`SINGLE`, `PREFIX`, `MANIFEST`) +- `source` (`S3` in current spec) +- `uri` +- `manifest` +- `filters.include_globs` +- `filters.exclude_globs` +- `auth.aws.access_key_id` +- `auth.aws.secret_access_key` +- `auth.aws.session_token` + +Output subfields: +- `destination` +- `uri` +- `layout` (`SINGLE`, `PREFIX`, `ADJACENT`) +- `auth.aws.*` + +Config subfields: +- `language` +- `word_time_offsets` +- `channel_separation` +- `diarization` +- `profanity_filter` +- `output_format` +- `segmentation_mode` + +Optional: +- `reference_id` +- `notifications.webhook_url` +- `notifications.secret` + +Response keys: +- `job_id` +- `state` +- `submitted_at` + +### `GET /aiservices/scribe/jobs` + +Query params: +- `state` +- `page_size` +- `next_page_token` + +Response keys: +- `jobs` +- `next_page_token` + +### `GET /aiservices/scribe/jobs/{jobId}` + +Path params: +- `jobId` + +Response keys: +- `job_id` +- `state` +- `submitted_at` +- `summary` + +### `GET /aiservices/scribe/jobs/{jobId}/files` + +Path params: +- `jobId` + +Query params: +- `page_size` +- `next_page_token` + +Response keys: +- `files` +- `next_page_token` + +## Current Limits and Constraints Observed in Sources + +- Batch manifest max: `1000` file URIs. +- `include_globs` max items: `10`. +- `exclude_globs` max items: `10`. +- Audio/media formats called out in docs: `WAV`, `MP3`, `M4A`, `MP4`. +- Batch job rate limit label in the OpenAPI description: `LIGHT`. diff --git a/partner-built/zoom-plugin/skills/scribe/references/environment-variables.md b/partner-built/zoom-plugin/skills/scribe/references/environment-variables.md new file mode 100644 index 00000000..fc4389d6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/references/environment-variables.md @@ -0,0 +1,41 @@ +# Environment Variables + +## Required for JWT Auth + +| Variable | Required | Description | +|----------|----------|-------------| +| `ZOOM_API_KEY` | Yes | Build-platform issuer key used in the JWT `iss` claim | +| `ZOOM_API_SECRET` | Yes | Build-platform signing secret for `HS256` JWT generation | + +Do not treat shell placeholders such as `${ZOOM_API_KEY}` as valid configured values. + +## Common App Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `PORT` | No | Local server port | +| `LANGUAGE` | No | Default language code such as `en-US` | + +## Batch / S3 Variables + +| Variable | Required for batch | Description | +|----------|--------------------|-------------| +| `S3_INPUT_URI` | Usually | Input prefix or file URI | +| `S3_OUTPUT_URI` | Usually | Output transcript destination | +| `AWS_ACCESS_KEY_ID` | If not using pre-signed access | AWS credential | +| `AWS_SECRET_ACCESS_KEY` | If not using pre-signed access | AWS credential | +| `AWS_SESSION_TOKEN` | Often | Temporary credential token | + +## Webhook Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `WEBHOOK_URL` | Optional | Public HTTPS callback for batch notifications | +| `WEBHOOK_SECRET` | Optional but recommended | HMAC secret used to verify Zoom callback signatures | + +## Where to Find These Values + +- Build-platform credentials: Zoom developer portal / Build app credential page. +- S3 URIs: your cloud storage path design. +- AWS credentials: IAM or STS-issued temporary credentials. +- Webhook URL: public HTTPS endpoint you control. diff --git a/partner-built/zoom-plugin/skills/scribe/references/samples-validation.md b/partner-built/zoom-plugin/skills/scribe/references/samples-validation.md new file mode 100644 index 00000000..27c963e0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/references/samples-validation.md @@ -0,0 +1,45 @@ +# Samples Validation + +Validated against: +- https://github.com/zoom/scribe-quickstart/ +- official docs pages under `docs/ai-services/` +- AI Services OpenAPI inventory at `api-hub/ai-services/methods/endpoints.json` +- Zoom blog context: + - `introducing-zoom-ai-services` + - `voice-insights-modernize-customer-support-with-scribe` + +## What the official quickstart confirms + +- Node/Express proxy architecture is a valid implementation model. +- Fast mode can be proxied as multipart upload handling on your server even though the docs show JSON examples. +- Batch mode commonly injects AWS credentials into request payloads. +- Webhook verification uses `x-zm-signature` + `x-zm-request-timestamp` with HMAC-SHA256 and `sha256=` prefix. +- The quickstart uses `ZOOM_API_KEY` / `ZOOM_API_SECRET` naming. + +## Useful implementation details from the sample + +- `multer` memory storage is enough for a small fast-mode demo. +- Batch helper routes are naturally expressed as: + - `POST /batch/jobs` + - `GET /batch/jobs` + - `GET /batch/jobs/:jobId` + - `GET /batch/jobs/:jobId/files` + - `DELETE /batch/jobs/:jobId` +- It is practical to keep one `generateJWT()` helper and inject the bearer token per request. + +## Caveats from the sample + +- It assumes Node `>=24`, which is stricter than many deployment environments actually need. Verify your runtime before copying that constraint unchanged. +- It uses environment-injected AWS credentials. Production pipelines may prefer pre-signed URLs or short-lived STS credentials only. +- The sample is an app demo, not a complete production reference for job retry policy, durable queues, or transcript storage. + +## What the blog posts add + +- They reinforce the highest-value downstream use cases: + - post-call summaries + - ticket enrichment + - compliance/audit logging + - searchable archives + - customer-support QA workflows +- They are useful for scenario framing, but not as authoritative API surface documentation. +- Keep endpoint and request-shape decisions anchored to the AI Services docs and API Hub inventory, not the blog wording. diff --git a/partner-built/zoom-plugin/skills/scribe/references/versioning-and-drift.md b/partner-built/zoom-plugin/skills/scribe/references/versioning-and-drift.md new file mode 100644 index 00000000..961b1b43 --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/references/versioning-and-drift.md @@ -0,0 +1,56 @@ +# Versioning and Drift + +## Naming Drift in Docs + +The current Zoom docs are inconsistent about credential naming: +- AI Services auth page uses `API key` / `API secret`. +- Build-platform credentials page uses `SDK key` / `SDK secret`. +- Quickstart code uses `ZOOM_API_KEY` / `ZOOM_API_SECRET`. + +Treat these as a portal/documentation naming drift issue and verify the current credential labels in the Zoom developer UI before changing production code. + +## Product Positioning Drift + +Scribe sits under `AI Services`, but related Zoom products may point users toward: +- RTMS for live meeting streams +- Meeting SDK Linux bots for visible in-meeting capture +- AI Companion / REST APIs for Zoom-generated summaries and transcripts +- blog or marketing material that frames Scribe inside broader speech/insights workflows + +Keep the guardrail clear: +- `scribe` = file/storage transcription service +- `rtms` = live media stream ingestion +- Meeting SDK Linux = participant bot capture / raw recording + +## Workflow-Claim Drift + +Some AI Services and Scribe blog material frames Scribe inside broader voice-insights workflows such as: +- post-call summaries +- ticket enrichment +- compliance logging +- searchable archives +- customer-support QA pipelines +- sentiment or keyword-driven downstream analytics + +These are valid architectural use cases, but they do not expand the current documented Scribe endpoint surface. + +Implementation rule: +- use `scribe` for transcript generation +- use your own downstream pipeline for sentiment, classification, QA scoring, or summarization +- do not infer undocumented real-time or analytics endpoints from blog phrasing alone + +## API Surface Drift Watchpoints + +Watch for changes in: +- storage providers beyond `S3` +- request field names in `config` +- webhook signature header conventions +- response summary/file schemas +- language / output-format support + +## Review Trigger + +Re-review this skill when: +- `api-hub/ai-services/methods/endpoints.json` changes +- AI Services docs rename Build/API credentials again +- quickstart sample changes webhook or upload patterns diff --git a/partner-built/zoom-plugin/skills/scribe/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/scribe/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..4f4c4b6f --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/scenarios/high-level-scenarios.md @@ -0,0 +1,86 @@ +# High-Level Scenarios + +## Scenario 1: On-Demand Upload Transcription + +Use fast mode when a user uploads one file and expects a transcript immediately. + +Flow: +1. Browser uploads file to your backend. +2. Backend generates Build JWT. +3. Backend calls `POST /aiservices/scribe/transcribe`. +4. Backend returns transcript JSON to the caller. + +Common downstream uses: +- post-call summaries +- ticket enrichment +- searchable clip libraries +- internal review or handoff notes + +## Scenario 2: Batch S3 Archive Transcription + +Use batch mode when call archives or media libraries already live in S3. + +Flow: +1. Build a batch request with input prefix and output prefix. +2. Submit `POST /aiservices/scribe/jobs`. +3. Track state by webhook or polling. +4. Read `/jobs/{jobId}/files` for per-file success/failure. +5. Ingest outputs into search, analytics, or storage. + +Common downstream uses: +- compliance and audit logging +- searchable webinar or podcast archives +- bulk transcript backfills +- QA scoring inputs + +## Scenario 3: Zoom Recording Export + Re-Transcription + +Use when you must re-process Zoom-managed recordings with your own transcript settings. + +Skill chain: +- `zoom-rest-api` to fetch/download recordings +- `scribe` to transcribe exported media + +Typical reasons: +- you need your own retention/search pipeline +- you need different transcript settings than Zoom-managed defaults +- you want to enrich recordings with your own summarization or tagging flow + +## Scenario 4: Compliance / QA Processing + +Use batch mode when transcripts must be generated offline for audits, QA scoring, or archival search. + +Prefer: +- `word_time_offsets=true` when reviewers need precise excerpts +- `channel_separation=true` for stereo call recordings +- webhook + queue ingestion instead of synchronous polling for large volumes + +## Scenario 5: Customer Support Voice-to-Insights Pipeline + +Use when support call recordings should feed operational analytics instead of stopping at raw transcript text. + +Flow: +1. Ingest call recordings from storage or exported meeting assets. +2. Transcribe with `scribe`. +3. Store transcript plus speaker/timing metadata. +4. Run downstream sentiment, keyword, escalation, or QA logic in your own pipeline. + +Guardrail: +- keep `scribe` focused on transcription +- do sentiment analysis, keyword detection, or scoring in downstream services after transcript generation + +## Scenario 6: Browser Microphone Incremental Transcript + +Use when a web page should capture microphone audio and show transcript updates every few seconds without switching to RTMS. + +Flow: +1. Browser captures microphone audio with `MediaRecorder`. +2. Browser flushes one chunk every `5 seconds`. +3. Backend accepts each chunk as a normal fast-mode upload through the async wrapper. +4. Frontend polls by request ID and appends transcript chunks in order. + +Guardrail: +- this is pseudo-streaming over repeated file uploads +- this is best kept as a lightweight demo or constrained fallback +- do not choose it first for a true live-transcription product +- if the requirement is truly live media stream ingestion or lower-latency continuous audio, route to `rtms` diff --git a/partner-built/zoom-plugin/skills/scribe/troubleshooting/common-drift-and-breaks.md b/partner-built/zoom-plugin/skills/scribe/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..14d75bbd --- /dev/null +++ b/partner-built/zoom-plugin/skills/scribe/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,134 @@ +# Common Drift and Breaks + +## 1. Auth fails even though credentials look correct + +Likely causes: +- wrong credential pair from the portal +- expired JWT +- mixing Build-platform credentials with non-Build Zoom app credentials +- valid-looking key/secret pair that is not authorized for AI Services Scribe + +Check: +- `iss` value +- `exp` window +- current credential labels in portal +- if the API responds with `{"code":124,"message":"Invalid Access token"}`, treat that as a real upstream auth failure, not a transport problem + +## 2. Fast mode request shape mismatch + +Docs show JSON with `file` URL, but the official quickstart also proxies multipart upload and forwards a `FormData` request. + +Use one clear model per service boundary: +- client upload -> your backend multipart +- backend upload proxy -> `multipart/form-data` to `/aiservices/scribe/transcribe` +- backend URL-based submit -> JSON body with `file` URL + +Symptoms: +- browser request stays pending for a long time +- backend eventually returns timeout or empty upstream reply + +Preferred fix: +- treat uploaded files and URL-based files as two separate request paths instead of forcing both through one JSON shape + +## 3. Fast mode returns `413 Request Entity Too Large` + +Likely cause: +- reverse proxy rejected the upload before the request reached your app + +Known deployment check: +- if nginx fronts the app, raise `client_max_body_size` to match or exceed your server-side upload limit + +Current Scribe fast-mode API limits: +- maximum file size: `100 MB` +- maximum duration: `2 hours` + +## 4. Fast mode returns `504 Gateway time-out` + +Likely cause: +- the request reached your backend, but synchronous processing took too long for the edge/proxy path + +Observed deployment behavior: +- public HTTPS can time out even when the same request path is valid and the backend is healthy +- observed hosted sample timings: + - ~17.2 MB MP4: ~26s + - ~38.6 MB MP4: ~26-37s + - ~59.2 MB MP4: ~32-34s backend completion, but some browser requests still timed out first + +Guardrail: +- use fast mode for smaller, interactive files +- use batch mode for large uploads or longer media where waiting synchronously through the web UI is brittle +- add request-level logging for: + - file name + - file size + - mime type + - upstream elapsed time + - response payload size and top-level keys + so you can tell whether the origin completed successfully while the browser/edge timed out +- for hosted UIs, wrap fast mode in an async request/polling flow instead of holding the browser open for the entire upstream response +- if nginx access logs show `499` while app logs later show `zoom_request_finished status: 200`, the transcription succeeded and only the browser-side request path was lost + +## 5. Batch job accepted but outputs never appear + +Likely causes: +- S3 URI/auth mismatch +- expired STS credentials +- output layout/URI mismatch +- webhook endpoint unreachable if you rely on callbacks + +Check: +- `/jobs/{jobId}` summary +- `/jobs/{jobId}/files` +- cloud storage permissions + +## 6. Webhook verification fails + +Current sample pattern uses: +- `x-zm-signature` +- `x-zm-request-timestamp` +- HMAC-SHA256 with `sha256=` prefix + +If verification fails: +- confirm raw body capture before JSON parsing +- confirm timestamp header was included in the signed string +- confirm the shared secret matches the job notification config + +## 7. Health check says credentials exist, but API calls still fail + +Likely cause: +- environment file contains literal placeholders such as `${ZOOM_API_KEY}` or `${ZOOM_API_SECRET}` + +Guardrail: +- only treat credentials as present if they are real values, not unresolved shell placeholders +- fail fast with a clear credential error before attempting Zoom calls + +## 8. Wrong product chosen + +Symptoms: +- trying to use Scribe for live in-meeting media +- trying to use RTMS for offline archive transcription + +Guardrail: +- file/storage transcription -> `scribe` +- live meeting media -> `rtms` + +## 9. Browser microphone chunk 1 works but later chunks are empty + +Likely cause: +- the browser emitted a valid first container chunk, but later `MediaRecorder` timeslice blobs were partial WebM/Opus clusters without fresh container headers + +Symptoms: +- chunk 1 transcribes normally +- chunk 2 onward returns empty transcript text or much weaker results +- auth and request flow still look healthy + +Preferred fix: +- do not rely on one long `MediaRecorder.start(timeslice)` session for standalone chunk uploads +- rotate the recorder per chunk instead: + - start recorder + - record one chunk window + - stop recorder + - upload that blob + - start a new recorder for the next chunk + +Guardrail: +- treat browser microphone pseudo-streaming as a file-container problem first, not a Scribe-language-model problem diff --git a/partner-built/zoom-plugin/skills/setup-zoom-mcp/SKILL.md b/partner-built/zoom-plugin/skills/setup-zoom-mcp/SKILL.md new file mode 100644 index 00000000..661b7b0b --- /dev/null +++ b/partner-built/zoom-plugin/skills/setup-zoom-mcp/SKILL.md @@ -0,0 +1,38 @@ +--- +name: setup-zoom-mcp +description: Decide when Zoom MCP is the right fit and produce a safe setup plan for Claude. Use when planning AI workflows over Zoom data, deciding between MCP and REST, or defining a hybrid MCP architecture. +argument-hint: "" +--- + +# /setup-zoom-mcp + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Plan a Zoom MCP workflow and decide when to use MCP alone versus a hybrid REST API + MCP architecture. + +## Usage + +```text +/setup-zoom-mcp $ARGUMENTS +``` + +## Workflow + +1. Determine whether the goal is deterministic automation, AI tool orchestration, or a hybrid. +2. If MCP is appropriate, identify the likely Zoom MCP surface and transport assumptions. +3. If MCP alone is not enough, define the REST API responsibilities separately. +4. Call out auth, scope, and client capability constraints. +5. End with a minimal proof-of-concept sequence. + +## Output + +- Recommended MCP strategy +- Connector expectations +- Hybrid boundaries if REST is also required +- Risks and setup notes +- Relevant skill links + +## Related Skills + +- [design-mcp-workflow](../design-mcp-workflow/SKILL.md) +- [choose-zoom-approach](../choose-zoom-approach/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/setup-zoom-oauth/SKILL.md b/partner-built/zoom-plugin/skills/setup-zoom-oauth/SKILL.md new file mode 100644 index 00000000..42cc72c3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/setup-zoom-oauth/SKILL.md @@ -0,0 +1,38 @@ +--- +name: setup-zoom-oauth +description: Implement Zoom authentication correctly. Use when setting up app credentials, choosing an OAuth grant, requesting scopes, handling token refresh, or debugging auth failures. +--- + +# /setup-zoom-oauth + +Use this skill when auth is the blocker or when auth choices will shape the entire integration. + +## Scope + +- App type selection +- OAuth grant selection +- Scope planning +- Token exchange and refresh +- Auth debugging and environment assumptions + +## Workflow + +1. Determine the app model and who is authorizing whom. +2. Choose the correct grant flow. +3. Identify minimum scopes for the user flow. +4. Define token storage and refresh behavior. +5. Route into the deepest relevant reference docs only after the above is clear. + +## Primary References + +- [oauth](../oauth/SKILL.md) +- [general](../general/SKILL.md) +- [rest-api](../rest-api/SKILL.md) + +## Common Mistakes + +- Picking a grant before clarifying the actor and tenant model +- Asking for broad scopes before confirming the exact workflow +- Forgetting refresh-token behavior and token lifecycle handling +- Reusing an old refresh token after a successful refresh instead of storing the newly returned one +- Treating auth failures as API failures without checking app configuration first diff --git a/partner-built/zoom-plugin/skills/start/SKILL.md b/partner-built/zoom-plugin/skills/start/SKILL.md new file mode 100644 index 00000000..1cf44572 --- /dev/null +++ b/partner-built/zoom-plugin/skills/start/SKILL.md @@ -0,0 +1,46 @@ +--- +name: start +description: Start here for any Zoom integration or app idea. Use when you need to choose the right Zoom surface, shape the architecture, or route into the correct implementation skill without reading the whole Zoom doc set first. +--- + +# Start + +Use this as the default entry skill for the plugin. + +## What This Skill Does + +- Classifies the request by job-to-be-done, not by product name alone +- Routes into the right implementation skill +- Pulls in product-specific Zoom references only after the route is clear +- Prevents common early mistakes, especially Meeting SDK vs Video SDK and REST API vs MCP confusion + +## Routing Table + +| If the user wants to... | Route to | +|---|---| +| Choose the right Zoom surface for a new project | [plan-zoom-product](../plan-zoom-product/SKILL.md) | +| Set up OAuth, tokens, scopes, or app credentials | [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) | +| Embed or customize a Zoom meeting flow | [build-zoom-meeting-app](../build-zoom-meeting-app/SKILL.md) | +| Build a bot, recorder, or real-time meeting processor | [build-zoom-bot](../build-zoom-bot/SKILL.md) | +| Use Zoom-hosted MCP for AI workflows | [setup-zoom-mcp](../setup-zoom-mcp/SKILL.md) | +| Debug a broken integration | [debug-zoom](../debug-zoom/SKILL.md) | + +## Supporting Zoom References + +Use these only after selecting the workflow: + +- [general](../general/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [websockets](../websockets/SKILL.md) +- [oauth](../oauth/SKILL.md) +- [zoom-mcp](../zoom-mcp/SKILL.md) + +## Operating Rules + +1. Prefer one clear recommendation over a product catalog dump. +2. Ask a short clarifier only when the route is genuinely ambiguous. +3. Keep the first response architectural and actionable, then go deep. +4. Pull in deeper references only when they directly help the current decision or implementation. diff --git a/partner-built/zoom-plugin/skills/team-chat/RUNBOOK.md b/partner-built/zoom-plugin/skills/team-chat/RUNBOOK.md new file mode 100644 index 00000000..fa155e47 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/RUNBOOK.md @@ -0,0 +1,85 @@ +# Team Chat 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Team Chat failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Integration Type + +- User type (Team Chat API): user OAuth + `/v2/chat/users/...` +- Bot type (Chatbot API): client credentials + `/v2/im/chat/messages` + +If this is wrong, everything else will fail. + +## 2) Confirm OAuth Endpoints + +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +If token requests hit `/oauth/token`, expect 404/HTML. + +## 3) Confirm Runtime Env Loading + +If credentials are split by mode, verify your server loads the actual files at runtime: + +- `project/team-chat-api/.env` +- `project/chatbot-api/.env` + +Do not assume root `.env` is enough. + +## 4) Confirm App Routes + Reverse Proxy + +- Current demo pages: + - `/team-chat/user-demo` + - `/team-chat/bot-demo` +- API path should resolve: `/team-chat/api/*` + +If browser calls old routes (`/api/channel/*`) and gets 404, either update frontend or keep compatibility routes. + +## 5) Run Curl Probes + +Use backend probes before browser debugging. + +```bash +TEAM_CHAT_BASE_URL="http://YOUR_HOST:YOUR_PORT" + +curl -sS "$TEAM_CHAT_BASE_URL/team-chat/api/config" +curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/bot/token" +curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/channel/list" +``` + +Expected: +- `api/config` shows required flags as configured. +- `api/bot/token` should return JSON (200 or actionable 4xx), never HTML 404 page. +- `api/channel/list` returns validation errors or data, not generic 404. + +## 6) Browser-Specific Reality Check + +`ERR_BLOCKED_BY_CLIENT` usually means extension/adblock/privacy filter interference. + +- Re-test in Incognito. +- Temporarily disable blockers for host. +- Validate with curl first. + +## 7) User OAuth Callback Flow (In-App) + +For user-demo, avoid manual copy/paste flow: + +1. UI button triggers backend authorize URL generation with `state`. +2. Browser redirects to Zoom consent page. +3. Callback validates `state` and exchanges `code` server-side. +4. Token is stored where UI expects (session/db/local storage for demo). +5. Redirect back to user-demo. + +If callback returns but token is missing, focus on `state` validation and persistence path. + +## 8) Fast Decision Tree + +- **404 on bot token** -> check token URL (`/oauth/token`), then proxy path. +- **All channel APIs 404** -> route mismatch (old UI vs new backend routes). +- **OAuth works but sends fail** -> wrong scopes or app type mismatch. +- **Works by curl but fails in browser** -> blocked client/cached old JS. diff --git a/partner-built/zoom-plugin/skills/team-chat/SKILL.md b/partner-built/zoom-plugin/skills/team-chat/SKILL.md new file mode 100644 index 00000000..37b2138d --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/SKILL.md @@ -0,0 +1,687 @@ +--- +name: build-zoom-team-chat-app +description: "Reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash commands, or chat webhooks." +triggers: + - "zoom team chat" + - "zoom chatbot" + - "zoom messaging" + - "team chat api" + - "chatbot api" + - "zoom slash commands" + - "zoom chat integration" +--- + +# /build-zoom-team-chat-app + +Background reference for Zoom Team Chat integrations. Use this after the workflow is clear, especially when the Team Chat API versus Chatbot API distinction matters. + +## Read This First (Critical) + +There are two different integration types and they are not interchangeable: + +1. **Team Chat API (user type)** + - Sends messages as a real authenticated user + - Uses **User OAuth** (`authorization_code`) + - Endpoint family: `/v2/chat/users/...` + +2. **Chatbot API (bot type)** + - Sends messages as your bot identity + - Uses **Client Credentials** (`client_credentials`) + - Endpoint family: `/v2/im/chat/messages` + +If you choose the wrong type early, auth/scopes/endpoints all mismatch and implementation fails. + +**Official Documentation**: https://developers.zoom.us/docs/team-chat/ +**Chatbot Documentation**: https://developers.zoom.us/docs/team-chat/chatbot/extend/ +**API Reference**: https://developers.zoom.us/docs/api/rest/reference/chatbot/ + +## Quick Links + +**New to Team Chat? Follow this path:** + +1. **[Get Started](get-started.md)** - End-to-end fast path (user type vs bot type) +2. **[Choose Your API](concepts/api-selection.md)** - Team Chat API vs Chatbot API +3. **[Environment Setup](concepts/environment-setup.md)** - Credentials, scopes, app configuration +4. **[OAuth Setup](examples/oauth-setup.md)** - Complete authentication flow +5. **[Send First Message](examples/send-message.md)** - Working code to send messages + +**Reference:** +- **[Chatbot Message Cards](references/message-cards.md)** - Complete card component reference +- **[Webhook Events](references/webhook-events.md)** - All webhook event types +- **[API Reference](references/api-reference.md)** - Endpoints, methods, parameters +- **[Sample Applications](references/samples.md)** - 10+ official sample apps +- **Integrated Index** - see the section below in this file + +**Having issues?** +- Authentication errors → [OAuth Troubleshooting](troubleshooting/oauth-issues.md) +- Webhook not receiving events → [Webhook Setup Guide](troubleshooting/webhook-issues.md) +- Messages not sending → [Common Issues](troubleshooting/common-issues.md) +- Start with quick checks → [5-Minute Runbook](RUNBOOK.md) + +**OAuth endpoint sanity check:** +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` +- If `/oauth/token` returns 404/HTML, use `https://zoom.us/oauth/token`. + +**Building Interactive Bots?** +- [Button Actions](examples/button-actions.md) - Handle button clicks +- [Form Submissions](examples/form-submissions.md) - Process form data +- [Slash Commands](examples/slash-commands.md) - Create custom commands + +## Quick Decision: Which API? + +| Use Case | API to Use | +|----------|------------| +| Send notifications from scripts/CI/CD | **Team Chat API** | +| Automate messages as a user | **Team Chat API** | +| Build an interactive chatbot | **Chatbot API** | +| Respond to slash commands | **Chatbot API** | +| Create messages with buttons/forms | **Chatbot API** | +| Handle user interactions | **Chatbot API** | + +### Team Chat API (User-Level) +- Messages appear as sent by **authenticated user** +- Requires **User OAuth** (authorization_code flow) +- Endpoint: `POST https://api.zoom.us/v2/chat/users/me/messages` +- Scopes: `chat_message:write`, `chat_channel:read` + +### Chatbot API (Bot-Level) +- Messages appear as sent by your **bot** +- Requires **Client Credentials** grant +- Endpoint: `POST https://api.zoom.us/v2/im/chat/messages` +- Scopes: `imchat:bot` (auto-added) +- **Rich cards**: buttons, forms, dropdowns, images + +## Prerequisites + +### System Requirements + +- Zoom account +- Account owner, admin, or **Zoom for developers** role enabled + - To enable: **User Management** → **Roles** → **Role Settings** → **Advanced features** → Enable **Zoom for developers** + +### Create Zoom App + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select **General App** (OAuth) + +> ⚠️ **Do NOT use Server-to-Server OAuth** - S2S apps don't have the Chatbot/Team Chat feature. Only General App (OAuth) supports chatbots. + +### Required Credentials + +From Zoom Marketplace → Your App: + +| Credential | Location | Used By | +|------------|----------|---------| +| Client ID | App Credentials → Development | Both APIs | +| Client Secret | App Credentials → Development | Both APIs | +| Account ID | App Credentials → Development | Chatbot API | +| Bot JID | Features → Chatbot → Bot Credentials | Chatbot API | +| Secret Token | Features → Team Chat Subscriptions | Chatbot API | + +**See**: [Environment Setup Guide](concepts/environment-setup.md) for complete configuration steps. + +## Quick Start: Team Chat API + +Send a message as a user: + +```javascript +// 1. Get access token via OAuth +const accessToken = await getOAuthToken(); // See examples/oauth-setup.md + +// 2. Send message to channel +const response = await fetch('https://api.zoom.us/v2/chat/users/me/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + message: 'Hello from CI/CD pipeline!', + to_channel: 'CHANNEL_ID' + }) +}); + +const data = await response.json(); +// { "id": "msg_abc123", "date_time": "2024-01-15T10:30:00Z" } +``` + +**Complete example**: [Send Message Guide](examples/send-message.md) + +## Quick Start: Chatbot API + +Build an interactive chatbot: + +```javascript +// 1. Get chatbot token (client_credentials) +async function getChatbotToken() { + const credentials = Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).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=client_credentials' + }); + + return (await response.json()).access_token; +} + +// 2. Send chatbot message with buttons +const response = await fetch('https://api.zoom.us/v2/im/chat/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + robot_jid: process.env.ZOOM_BOT_JID, + to_jid: payload.toJid, // From webhook + account_id: payload.accountId, // From webhook + content: { + head: { + text: 'Build Notification', + sub_head: { text: 'CI/CD Pipeline' } + }, + body: [ + { type: 'message', text: 'Deployment successful!' }, + { + type: 'fields', + items: [ + { key: 'Branch', value: 'main' }, + { key: 'Commit', value: 'abc123' } + ] + }, + { + type: 'actions', + items: [ + { text: 'View Logs', value: 'view_logs', style: 'Primary' }, + { text: 'Dismiss', value: 'dismiss', style: 'Default' } + ] + } + ] + } + }) +}); +``` + +**Complete example**: [Chatbot Setup Guide](examples/chatbot-setup.md) + +## Key Features + +### Team Chat API + +| Feature | Description | +|---------|-------------| +| **Send Messages** | Post messages to channels or direct messages | +| **List Channels** | Get user's channels with metadata | +| **Create Channels** | Create public/private channels programmatically | +| **Threaded Replies** | Reply to specific messages in threads | +| **Edit/Delete** | Modify or remove messages | + +### Chatbot API + +| Feature | Description | +|---------|-------------| +| **Rich Message Cards** | Headers, images, fields, buttons, forms | +| **Slash Commands** | Custom `/commands` trigger webhooks | +| **Button Actions** | Interactive buttons with webhook callbacks | +| **Form Submissions** | Collect user input with forms | +| **Dropdown Selects** | Channel, member, date/time pickers | +| **LLM Integration** | Easy integration with Claude, GPT, etc. | + +## Webhook Events (Chatbot API) + +| Event | Trigger | Use Case | +|-------|---------|----------| +| `bot_notification` | User messages bot or uses slash command | Process commands, integrate LLM | +| `bot_installed` | Bot added to account | Initialize bot state | +| `interactive_message_actions` | Button clicked | Handle button actions | +| `chat_message.submit` | Form submitted | Process form data | +| `app_deauthorized` | Bot removed | Cleanup | + +**See**: [Webhook Events Reference](references/webhook-events.md) + +## Message Card Components + +Build rich interactive messages with these components: + +| Component | Description | +|-----------|-------------| +| **header** | Title and subtitle | +| **message** | Plain text | +| **fields** | Key-value pairs | +| **actions** | Buttons (Primary, Danger, Default styles) | +| **section** | Colored sidebar grouping | +| **attachments** | Images with links | +| **divider** | Horizontal line | +| **form_field** | Text input | +| **dropdown** | Select menu | +| **date_picker** | Date selection | + +**See**: [Message Cards Reference](references/message-cards.md) for complete component catalog + +## Architecture Patterns + +### Chatbot Lifecycle + +``` +User types /command → Webhook receives bot_notification + ↓ + payload.cmd = "user's input" + ↓ + Process command + ↓ + Send response via sendChatbotMessage() +``` + +### LLM Integration Pattern + +```javascript +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // 1. Call your LLM + const llmResponse = await callClaude(cmd); + + // 2. Send response back + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: llmResponse }] + }); +} +``` + +**See**: [LLM Integration Guide](examples/llm-integration.md) + +## Sample Applications + +| Sample | Description | Link | +|--------|-------------|------| +| **Chatbot Quickstart** | Official tutorial (recommended start) | [GitHub](https://github.com/zoom/chatbot-nodejs-quickstart) | +| **Claude Chatbot** | AI chatbot with Anthropic Claude | [GitHub](https://github.com/zoom/zoom-chatbot-claude-sample) | +| **Unsplash Chatbot** | Image search with database | [GitHub](https://github.com/zoom/unsplash-chatbot) | +| **ERP Chatbot** | Oracle ERP with scheduled alerts | [GitHub](https://github.com/zoom/zoom-erp-chatbot-sample) | +| **Task Manager** | Full CRUD app | [GitHub](https://github.com/zoom/task-manager-sample) | + +**See**: [Sample Applications Guide](references/samples.md) for analysis of all 10 samples + +## Common Operations + +### Send Message to Channel + +```javascript +// Team Chat API +await fetch('https://api.zoom.us/v2/chat/users/me/messages', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ + message: 'Hello!', + to_channel: 'CHANNEL_ID' + }) +}); +``` + +### Handle Button Click + +```javascript +// Webhook handler +case 'interactive_message_actions': { + const { actionItem, toJid, accountId } = payload; + + if (actionItem.value === 'approve') { + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '✅ Approved!' }] + }); + } +} +``` + +### Verify Webhook Signature + +```javascript +function verifyWebhook(req) { + const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`; + const hash = crypto.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(message) + .digest('hex'); + return req.headers['x-zm-signature'] === `v0=${hash}`; +} +``` + +## Deployment + +### ngrok for Local Development + +```bash +# Install ngrok +npm install -g ngrok + +# Expose local server +ngrok http 4000 + +# Use HTTPS URL as Bot Endpoint URL in Zoom Marketplace +# Example: https://abc123.ngrok.io/webhook +``` + +### Production Deployment + +**See**: [Deployment Guide](concepts/deployment.md) for: +- Nginx reverse proxy setup +- Base path configuration +- OAuth redirect URI setup + +## Limitations + +| Limit | Value | +|-------|-------| +| Message length | 4,096 characters | +| File size | 512 MB | +| Members per channel | 10,000 | +| Channels per user | 500 | + +## Security Best Practices + +1. **Verify webhook signatures** - Always validate using `x-zm-signature` header +2. **Sanitize messages** - Limit to 4096 chars, remove control characters +3. **Validate JIDs** - Check format: `user@domain` or `channel@domain` +4. **Environment variables** - Never hardcode credentials +5. **Use HTTPS** - Required for production webhooks + +**See**: [Security Best Practices](concepts/security.md) + +## Complete Documentation Library + +### Core Concepts (Start Here!) +- **[API Selection Guide](concepts/api-selection.md)** - Choose Team Chat API vs Chatbot API +- **[Environment Setup](concepts/environment-setup.md)** - Complete credentials guide +- **[Authentication Flows](concepts/authentication.md)** - OAuth vs Client Credentials +- **[Webhook Architecture](concepts/webhooks.md)** - How webhooks work +- **[Message Card Structure](concepts/message-structure.md)** - Card component hierarchy + +### Complete Examples +- **[OAuth Setup](examples/oauth-setup.md)** - Full OAuth implementation +- **[Send Message](examples/send-message.md)** - Team Chat API message sending +- **[Chatbot Setup](examples/chatbot-setup.md)** - Complete chatbot with webhooks +- **[Button Actions](examples/button-actions.md)** - Handle interactive buttons +- **[Form Submissions](examples/form-submissions.md)** - Process form data +- **[Slash Commands](examples/slash-commands.md)** - Create custom commands +- **[LLM Integration](examples/llm-integration.md)** - Claude/GPT integration +- **[Scheduled Alerts](examples/scheduled-alerts.md)** - Cron + incoming webhooks +- **[Channel Management](examples/channel-management.md)** - Create/manage channels + +### References +- **[API Reference](references/api-reference.md)** - All endpoints and methods +- **[Webhook Events](references/webhook-events.md)** - Complete event reference +- **[Message Cards](references/message-cards.md)** - All card components +- **[Sample Applications](references/samples.md)** - Analysis of 10 official samples +- **[Error Codes](references/error-codes.md)** - Error handling guide + +### Troubleshooting +- **[OAuth Issues](troubleshooting/oauth-issues.md)** - Authentication failures +- **[Webhook Issues](troubleshooting/webhook-issues.md)** - Webhook debugging +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/team-chat/ +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/chatbot/ +- **Dev Forum**: https://devforum.zoom.us/ +- **App Marketplace**: https://marketplace.zoom.us/ + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +Complete navigation guide for the Zoom Team Chat skill. + +## Quick Start Paths + +- Start here: [Get Started](get-started.md) +- Fast troubleshooting first: [5-Minute Runbook](RUNBOOK.md) + +### Path 1: Team Chat API (User-Level Messaging) + +For sending messages as a user account. + +1. [API Selection Guide](concepts/api-selection.md) - Confirm Team Chat API is right +2. [Environment Setup](concepts/environment-setup.md) - Get credentials +3. [OAuth Setup Example](examples/oauth-setup.md) - Implement authentication +4. [Send Message Example](examples/send-message.md) - Send your first message + +### Path 2: Chatbot API (Interactive Bots) + +For building interactive chatbots with rich messages. + +1. [API Selection Guide](concepts/api-selection.md) - Confirm Chatbot API is right +2. [Environment Setup](concepts/environment-setup.md) - Get credentials (including Bot JID) +3. [Webhook Architecture](concepts/webhooks.md) - Understand webhook events +4. [Chatbot Setup Example](examples/chatbot-setup.md) - Build your first bot +5. [Message Cards Reference](references/message-cards.md) - Create rich messages + +## Core Concepts + +Essential understanding for both APIs. + +| Document | Description | +|----------|-------------| +| [API Selection Guide](concepts/api-selection.md) | Choose Team Chat API vs Chatbot API | +| [Environment Setup](concepts/environment-setup.md) | Complete credentials and app configuration | +| [Authentication Flows](concepts/authentication.md) | OAuth vs Client Credentials | +| [Webhook Architecture](concepts/webhooks.md) | How webhooks work (Chatbot API) | +| [Message Card Structure](concepts/message-structure.md) | Card component hierarchy | +| [Deployment Guide](concepts/deployment.md) | Production deployment strategies | +| [Security Best Practices](concepts/security.md) | Secure your integration | + +## Complete Examples + +Working code for common scenarios. + +### Authentication +| Example | Description | +|---------|-------------| +| [OAuth Setup](examples/oauth-setup.md) | User OAuth flow implementation | +| [Token Management](examples/token-management.md) | Refresh tokens, expiration handling | + +### Basic Operations +| Example | Description | +|---------|-------------| +| [Send Message](examples/send-message.md) | Team Chat API message sending | +| [Chatbot Setup](examples/chatbot-setup.md) | Complete chatbot with webhooks | +| [List Channels](examples/channel-management.md) | Get user's channels | +| [Create Channel](examples/channel-management.md) | Create public/private channels | + +### Interactive Features (Chatbot API) +| Example | Description | +|---------|-------------| +| [Button Actions](examples/button-actions.md) | Handle button clicks | +| [Form Submissions](examples/form-submissions.md) | Process form data | +| [Slash Commands](examples/slash-commands.md) | Create custom commands | +| [Dropdown Selects](examples/dropdown-selects.md) | Channel/member pickers | + +### Advanced Integration +| Example | Description | +|---------|-------------| +| [LLM Integration](examples/llm-integration.md) | Integrate Claude/GPT | +| [Scheduled Alerts](examples/scheduled-alerts.md) | Cron + incoming webhooks | +| [Database Integration](examples/database-integration.md) | Store conversation state | +| [Multi-Step Workflows](examples/multi-step-workflows.md) | Complex user interactions | + +## References + +### API Documentation +| Reference | Description | +|-----------|-------------| +| [API Reference](references/api-reference.md) | Pointers and common endpoints | +| [Webhook Events](references/webhook-events.md) | Event types and handling checklist | +| [Message Cards](references/message-cards.md) | All card components | +| [Error Codes](references/error-codes.md) | Error handling guide | + +### Sample Applications +| Reference | Description | +|-----------|-------------| +| [Sample Applications](references/samples.md) | Sample app index/notes | + +### Field Guides +| Reference | Description | +|-----------|-------------| +| [JID Formats](references/jid-formats.md) | Understanding JID identifiers | +| [Scopes Reference](references/scopes.md) | Common scopes | +| [Rate Limits](references/rate-limits.md) | Throttling guidance | + +## Troubleshooting + +| Guide | Description | +|-------|-------------| +| [Common Issues](troubleshooting/common-issues.md) | Quick diagnostics and solutions | +| [OAuth Issues](troubleshooting/oauth-issues.md) | Authentication failures | +| [Webhook Issues](troubleshooting/webhook-issues.md) | Webhook debugging | +| [Message Issues](troubleshooting/message-issues.md) | Message sending problems | +| [Deployment Issues](troubleshooting/deployment-issues.md) | Production problems | + +## Architecture Patterns + +### Chatbot Lifecycle + +``` +User Action → Webhook → Process → Response +``` + +### LLM Integration Pattern + +``` +User Input → Chatbot receives → Call LLM → Send response +``` + +### Approval Workflow Pattern + +``` +Request → Send card with buttons → User clicks → Update status → Notify +``` + +## Common Use Cases + +### Notifications +- CI/CD build notifications +- Server monitoring alerts +- Scheduled reports +- System health checks + +### Workflows +- Approval requests +- Task assignment +- Status updates +- Form submissions + +### Integrations +- LLM-powered assistants +- Database queries +- External API integration +- File/image sharing + +### Automation +- Scheduled messages +- Auto-responses +- Data collection +- Report generation + +## Resource Links + +### Official Documentation +- **[Team Chat Docs](https://developers.zoom.us/docs/team-chat/)** - Official overview +- **[Chatbot Docs](https://developers.zoom.us/docs/team-chat/chatbot/extend/)** - Chatbot guide +- **[API Reference](https://developers.zoom.us/docs/api/rest/reference/chatbot/)** - REST API docs +- **[App Marketplace](https://marketplace.zoom.us/)** - Create and manage apps + +### Sample Code +- **[Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart)** - Official tutorial +- **[Claude Chatbot](https://github.com/zoom/zoom-chatbot-claude-sample)** - AI integration +- **[Unsplash Chatbot](https://github.com/zoom/unsplash-chatbot)** - Image search bot +- **[ERP Chatbot](https://github.com/zoom/zoom-erp-chatbot-sample)** - Enterprise integration +- **[Task Manager](https://github.com/zoom/task-manager-sample)** - Full CRUD app + +### Tools +- **[App Card Builder](https://appssdk.zoom.us/cardbuilder/)** - Visual card designer +- **[ngrok](https://ngrok.com/)** - Local webhook testing +- **[Postman](https://www.postman.com/)** - API testing + +### Community +- **[Developer Forum](https://devforum.zoom.us/)** - Ask questions +- **[GitHub Discussions](https://github.com/zoom)** - Community support +- **[Developer Support](https://devsupport.zoom.us)** - Official support + +## Documentation Status + +### ✅ Complete +- Main skill.md entry point +- API Selection Guide +- Environment Setup +- Webhook Architecture +- Chatbot Setup Example (complete working code) +- Message Cards Reference +- Common Issues Troubleshooting + +### 📝 Pending (High Priority) +- OAuth Setup Example +- Send Message Example +- Button Actions Example +- LLM Integration Example +- Webhook Events Reference +- API Reference +- Sample Applications Analysis + +### 📋 Planned (Lower Priority) +- Form Submissions Example +- Channel Management Examples +- Database Integration Example +- Error Codes Reference +- Rate Limits Guide +- Deployment troubleshooting + +## Getting Started Checklist + +### For Team Chat API + +- [ ] Read [API Selection Guide](concepts/api-selection.md) +- [ ] Complete [Environment Setup](concepts/environment-setup.md) +- [ ] Obtain Client ID, Client Secret +- [ ] Add required scopes +- [ ] Implement OAuth flow +- [ ] Send first message + +### For Chatbot API + +- [ ] Read [API Selection Guide](concepts/api-selection.md) +- [ ] Complete [Environment Setup](concepts/environment-setup.md) +- [ ] Obtain Client ID, Client Secret, Bot JID, Secret Token, Account ID +- [ ] Enable Team Chat in Features +- [ ] Configure Bot Endpoint URL and Slash Command +- [ ] Set up ngrok for local testing +- [ ] Implement webhook handler +- [ ] Send first chatbot message + +## Version History + +- **v1.0** (2026-02-09) - Initial comprehensive documentation + - Core concepts (API selection, environment setup, webhooks) + - Complete chatbot setup example + - Message cards reference + - Common issues troubleshooting + +## Support + +Use this SKILL.md as the navigation hub for Team Chat API selection, setup, examples, and troubleshooting. + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/api-selection.md b/partner-built/zoom-plugin/skills/team-chat/concepts/api-selection.md new file mode 100644 index 00000000..c1c929b4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/api-selection.md @@ -0,0 +1,231 @@ +# API Selection Guide + +Zoom Team Chat offers **two distinct APIs** for different use cases. Choose the right one before you start building. + +## Critical First Decision + +Pick one of these integration types before writing code: + +- **User type** -> Team Chat API -> User OAuth -> `/v2/chat/users/...` +- **Bot type** -> Chatbot API -> Client Credentials -> `/v2/im/chat/messages` + +Most implementation issues come from mixing user-type auth with bot-type endpoints (or the opposite). + +## Quick Decision Matrix + +| Use Case | API to Use | Messages Appear As | +|----------|------------|-------------------| +| Send notifications from scripts/CI/CD | **Team Chat API** | Authenticated user | +| Automate messages as a user | **Team Chat API** | Authenticated user | +| Build an interactive chatbot | **Chatbot API** | Your bot | +| Respond to slash commands | **Chatbot API** | Your bot | +| Create messages with buttons/forms | **Chatbot API** | Your bot | +| Handle user interactions | **Chatbot API** | Your bot | + +## Team Chat API (User-Level Messaging) + +### What It Is + +The Team Chat API allows your application to send messages **as an authenticated user**. Messages appear in Team Chat as if the user sent them manually. + +### When to Use + +✅ **Use Team Chat API when:** +- You want to send simple text messages programmatically +- Messages should appear as sent by a specific user +- You're building CI/CD notifications +- You're automating user-level messaging +- You don't need interactive components (buttons, forms) + +### Key Characteristics + +| Aspect | Details | +|--------|---------| +| **Authentication** | User OAuth (authorization_code flow) | +| **Endpoint** | `POST https://api.zoom.us/v2/chat/users/me/messages` | +| **Message Format** | Plain text or markdown | +| **Scopes** | `chat_message:write`, `chat_channel:read` | +| **User Experience** | Messages appear from the authenticated user | + +### Example Use Cases + +1. **CI/CD Notifications** + ``` + User: "Build #123 completed successfully" + ``` + +2. **Automated Reporting** + ``` + User: "Daily sales report: $10,000" + ``` + +3. **Task Reminders** + ``` + User: "Reminder: Team meeting in 15 minutes" + ``` + +## Chatbot API (Bot-Level Interactions) + +### What It Is + +The Chatbot API allows your application to send messages **as a bot**. Bots can send rich, interactive messages with buttons, forms, images, and handle user interactions via webhooks. + +### When to Use + +✅ **Use Chatbot API when:** +- You want to build an interactive chatbot +- You need rich message formatting (cards, buttons, forms) +- You want to handle slash commands (e.g., `/weather`) +- You need to respond to button clicks or form submissions +- You're integrating LLMs (Claude, GPT, etc.) +- You want scheduled notifications + +### Key Characteristics + +| Aspect | Details | +|--------|---------| +| **Authentication** | Client Credentials grant | +| **Endpoint** | `POST https://api.zoom.us/v2/im/chat/messages` | +| **Message Format** | Rich cards with components | +| **Scopes** | `imchat:bot` (auto-added) | +| **User Experience** | Messages appear from your bot | +| **Interactivity** | Buttons, forms, dropdowns, webhooks | + +### Example Use Cases + +1. **Support Bot** + ``` + Bot: "How can I help you?" + [Help Center] [Contact Support] [Report Bug] + ``` + +2. **Approval Workflow** + ``` + Bot: "Expense Report: $500" + Branch: main + Requester: John + [Approve] [Reject] + ``` + +3. **AI Assistant** + ``` + User: "/ask What's the weather?" + Bot: "The weather in San Francisco is 72°F and sunny." + ``` + +## Feature Comparison + +| Feature | Team Chat API | Chatbot API | +|---------|---------------|-------------| +| **Plain Text Messages** | ✅ | ✅ | +| **Markdown** | ✅ | ✅ | +| **Rich Cards** | ❌ | ✅ | +| **Buttons** | ❌ | ✅ | +| **Forms** | ❌ | ✅ | +| **Dropdowns** | ❌ | ✅ | +| **Images** | ✅ (basic) | ✅ (rich) | +| **Slash Commands** | ❌ | ✅ | +| **Webhooks** | ❌ | ✅ | +| **Button Click Handling** | ❌ | ✅ | +| **Form Submissions** | ❌ | ✅ | + +## Authentication Comparison + +### Team Chat API (User OAuth) + +**Flow**: authorization_code +**Requires**: User login and consent +**Token Scope**: User's data only + +```javascript +// Step 1: Redirect user to OAuth consent page +const authUrl = `https://zoom.us/oauth/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}`; + +// Step 2: Exchange auth code for access token +const tokens = await exchangeCodeForToken(code); + +// Step 3: Use access token to send messages +fetch('https://api.zoom.us/v2/chat/users/me/messages', { + headers: { 'Authorization': `Bearer ${tokens.access_token}` } +}); +``` + +### Chatbot API (Client Credentials) + +**Flow**: client_credentials +**Requires**: No user login +**Token Scope**: Bot actions only + +```javascript +// Step 1: Get bot token (no user interaction) +const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); +const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { 'Authorization': `Basic ${credentials}` }, + body: 'grant_type=client_credentials' +}); + +const { access_token } = await response.json(); + +// Step 2: Use access token to send bot messages +fetch('https://api.zoom.us/v2/im/chat/messages', { + headers: { 'Authorization': `Bearer ${access_token}` } +}); +``` + +## Can I Use Both? + +**Yes!** You can use both APIs in the same application. + +**Example**: Task management app +- **Team Chat API**: User creates a task → message appears as "User created task #123" +- **Chatbot API**: Bot sends reminders → "Task #123 is due today [View] [Snooze]" + +## Decision Tree + +``` +Need rich interactive messages? +├─ Yes → Chatbot API +└─ No + └─ Need webhooks (slash commands, button clicks)? + ├─ Yes → Chatbot API + └─ No + └─ Messages should appear as user? + ├─ Yes → Team Chat API + └─ No → Chatbot API +``` + +## Common Misconceptions + +### ❌ "I need to use Server-to-Server OAuth for bots" +**Reality**: Chatbots require **General App (OAuth)**, not Server-to-Server OAuth. S2S apps don't support the Chatbot feature. + +### ❌ "Team Chat API can send buttons" +**Reality**: Only Chatbot API supports interactive components (buttons, forms, dropdowns). + +### ❌ "Chatbot API requires user login" +**Reality**: Chatbot API uses client_credentials flow (no user login needed). + +### ❌ "OAuth token endpoint is `/oauth/token`" +**Reality**: Use `https://zoom.us/oauth/token` for token exchange. Keep `https://zoom.us/oauth/authorize` for the user consent step. + +### ❌ "I can only use one API per app" +**Reality**: You can use both APIs in the same application. + +## Next Steps + +### If you chose **Team Chat API**: +1. [Environment Setup](environment-setup.md) - Get credentials +2. [OAuth Setup](../examples/oauth-setup.md) - Implement OAuth flow +3. [Send Message](../examples/send-message.md) - Send your first message + +### If you chose **Chatbot API**: +1. [Environment Setup](environment-setup.md) - Get credentials (including Bot JID) +2. [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot +3. [Webhook Architecture](webhooks.md) - Understand webhook events + +## Resources + +- [Official Team Chat API Docs](https://developers.zoom.us/docs/api/rest/reference/chat/) +- [Official Chatbot API Docs](https://developers.zoom.us/docs/api/rest/reference/chatbot/) +- [Chatbot Quickstart Sample](https://github.com/zoom/chatbot-nodejs-quickstart) diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/authentication.md b/partner-built/zoom-plugin/skills/team-chat/concepts/authentication.md new file mode 100644 index 00000000..5e9d27b5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/authentication.md @@ -0,0 +1,36 @@ +# Authentication Flows (Team Chat vs Chatbot) + +Zoom Team Chat integrations commonly use one of two auth models: + +## Team Chat API (user-level) + +Use **User OAuth (authorization code)** when you want messages/actions to appear as a user. + +- Typical endpoints: + - Send message (as the user): `POST /v2/chat/users/me/messages` +- Typical scopes: + - `chat_message:write` + - `chat_channel:read` (for listing channels) + +## Chatbot API (bot-level) + +Use **client credentials** when you want messages/actions to appear as a bot. + +- Typical endpoint: + - Send bot message: `POST /v2/im/chat/messages` +- Typical “scope”: + - `imchat:bot` (added by enabling Chatbot feature on the app) + +## Decision Checklist + +- If you need to post to a channel “as a bot” and handle slash command interactions: use **Chatbot API**. +- If you need to post “as the user” (and respect the user’s channel membership): use **Team Chat API**. + +## Common Pitfalls + +- **Server-to-Server OAuth** is not a fit for Zoom Team Chat chatbot features. +- Team Chat API calls require a user token with the right scopes; “invalid access token” errors are almost always missing scopes or wrong app type. +- OAuth URL split is easy to mix up: + - authorize step: `https://zoom.us/oauth/authorize` + - token step (all grant types): `https://zoom.us/oauth/token` +- In browser demos, complete OAuth end-to-end in app (state verify -> callback -> code exchange -> token store) to avoid copy/paste mistakes. diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/deployment.md b/partner-built/zoom-plugin/skills/team-chat/concepts/deployment.md new file mode 100644 index 00000000..755abeae --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/deployment.md @@ -0,0 +1,23 @@ +# Deployment Guide (Team Chat / Chatbot) + +## Basic Requirements + +- Your webhook endpoint must be reachable by Zoom (public HTTPS). +- Keep secrets out of the repo: + - `ZOOM_CLIENT_ID` + - `ZOOM_CLIENT_SECRET` + - `ZOOM_BOT_JID` (chatbot) + - `ZOOM_SECRET_TOKEN` (chatbot verification) + +## Recommended Production Setup + +- Run behind a reverse proxy (TLS termination). +- Use a persistent store for: + - OAuth tokens (Team Chat API) + - installation state (Chatbot API) + - idempotency keys for webhooks (avoid double-processing) + +## Local Testing + +- Use a tunneling tool to expose your local development host over HTTPS for webhook testing. +- Keep a "dev" app and "prod" app to avoid breaking production while iterating. diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/environment-setup.md b/partner-built/zoom-plugin/skills/team-chat/concepts/environment-setup.md new file mode 100644 index 00000000..2dfcac63 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/environment-setup.md @@ -0,0 +1,301 @@ +# Environment Setup + +Complete guide to configuring your Zoom Team Chat development environment, obtaining credentials, and setting up your app. + +## Prerequisites + +- Zoom account +- Account owner, admin, or **Zoom for developers** role enabled + +### Enable "Zoom for developers" Role + +If you don't have owner/admin privileges: + +1. Ask your admin to enable the **Zoom for developers** role +2. Navigate to: **User Management** → **Roles** → **Role Settings** → **Advanced features** +3. Enable **View** and **Edit** checkboxes for **Zoom for developers** + +![Zoom for developers role](https://developers.zoom.us/img/nextImageExportOptimizer/UBF-role-prerequisite-opt-1080.WEBP) + +## Step 1: Create Zoom App + +### 1.1 Access App Marketplace + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** + +### 1.2 Select App Type + +**Select**: **General App** (OAuth) + +> ⚠️ **CRITICAL**: Do NOT select "Server-to-Server OAuth" +> +> **Why**: Server-to-Server OAuth apps do NOT support the Team Chat/Chatbot features. Only General App (OAuth) supports chatbots and team chat integrations. + +## Step 2: Basic Information + +On the **Basic Info** page, configure your app: + +### 2.1 App Name + +Update the auto-generated app name: +- Click the edit icon (pencil) +- Enter your app name (e.g., "My Team Chat Bot") +- Click outside the field to save + +### 2.2 App Management Type + +Choose how your app is managed: + +| Type | Use Case | Token Flow | +|------|----------|------------| +| **Admin-managed** | Company-wide bots, notifications, helpdesk | Recommended for chatbots | +| **User-managed** | Personal bots, individual user tools | For user-specific apps | + +**For most chatbots**: Choose **Admin-managed** + +**Important**: App management type affects available features and scopes. If you change it later, reconfirm your selected features and scopes. + +### 2.3 App Credentials (Auto-generated) + +The build flow automatically generates: + +| Credential | Environment | +|------------|-------------| +| **Client ID** | Development & Production | +| **Client Secret** | Development & Production | + +**Note**: Development and production credentials are different. + +### 2.4 OAuth Information + +#### OAuth Redirect URL (Required) + +Enter your OAuth callback endpoint: + +**Local development**: +``` +http://YOUR_DEV_HOST:4000/auth/callback +``` + +**Production**: +``` +https://yourdomain.com/auth/callback +``` + +#### OAuth Allow Lists (Required) + +Add all URLs that Zoom should allow as valid OAuth redirects: + +**Examples**: +- Complete URL: `https://subdomain.domain.tld/path/oauth/callback` +- Base URL: `https://subdomain.domain.tld` + +## Step 3: Enable Team Chat (Chatbot API Only) + +> **Skip this step** if you're only using Team Chat API (user-level messaging) + +### 3.1 Navigate to Features Page + +Go to **Features** page → **Surface** tab + +### 3.2 Select Team Chat Product + +In **Select where to use your app**, check **Team Chat** + +### 3.3 Configure App URLs + +| Field | Value | Example | +|-------|-------|---------| +| **Home URL** | Your app's home page | `https://yourdomain.com` | +| **Domain Allow List** | URLs Zoom client should accept | `https://yourdomain.com` | + +### 3.4 Enable Team Chat Subscription + +Configure webhook settings: + +| Field | Value | Example | +|-------|-------|---------| +| **Slash Command** | Command to invoke bot | `/mybot` | +| **Bot Endpoint URL** | Webhook endpoint | `https://yourdomain.com/webhook` | + +> **Critical**: Your bot will NOT appear in Team Chat unless you enable Team Chat Subscription! + +## Step 4: Get Credentials + +### 4.1 App Credentials (Both APIs) + +Navigate to **App Credentials** → **Development**: + +| Credential | Where to Find | +|------------|---------------| +| **Client ID** | App Credentials → Development | +| **Client Secret** | App Credentials → Development (Click "View") | +| **Account ID** | App Credentials → Development | + +### 4.2 Bot JID (Chatbot API Only) + +> **Note**: Bot JID only appears AFTER enabling Chatbot in Features tab + +**To find Bot JID**: + +1. Go to **Features** tab in left sidebar +2. Ensure **Chatbot** toggle is **ON** +3. Click **Chatbot** section to expand +4. Scroll to **Bot Credentials** section +5. You'll see two JIDs: + - **Bot JID (Development)**: Use for testing + - **Bot JID (Production)**: Use for live apps + +**Format**: `v1abc123xyz@xmpp.zoom.us` + +### 4.3 Webhook Secret Token (Chatbot API Only) + +Navigate to **Features** → **Team Chat Subscriptions** → **Secret Token** + +This token is used to verify webhook signatures. + +### 4.4 Credentials Summary + +| Credential | Team Chat API | Chatbot API | Location | +|------------|---------------|-------------|----------| +| Client ID | ✅ Required | ✅ Required | App Credentials → Development | +| Client Secret | ✅ Required | ✅ Required | App Credentials → Development | +| Account ID | ❌ | ✅ Required | App Credentials → Development | +| Bot JID | ❌ | ✅ Required | Features → Chatbot → Bot Credentials | +| Secret Token | ❌ | ✅ Required | Features → Team Chat Subscriptions | + +## Step 5: Configure Scopes + +Navigate to **Scopes** page in your app. + +### Team Chat API Scopes + +Manually add these scopes: + +- `chat_message:write` - Send messages +- `chat_message:read` - Read messages +- `chat_channel:read` - List channels +- `chat_channel:write` - Create/manage channels + +### Chatbot API Scopes + +When you enable Team Chat Subscription, these scopes are **automatically added**: + +- `imchat:bot` - Basic chatbot functionality +- `team_chat:read:list_user_channels:admin` - List channels +- `team_chat:read:list_members:admin` - List members + +## Step 6: Create .env File + +### For Team Chat API (User-Level) + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback + +PORT=4000 +``` + +### For Chatbot API (Bot-Level) + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us +ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +``` + +### .env.example Template + +Create this file in your project root: + +```bash +# Zoom App Credentials (Required for both APIs) +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback + +# Chatbot Credentials (Required for Chatbot API only) +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# Server Configuration +PORT=4000 +``` + +## Step 7: Test Your App + +On the **Local Test** page: + +### 7.1 Add App to Your Account + +1. Click **Add App Now** +2. Click **Allow** to authorize the app +3. You'll be redirected to your OAuth redirect URL + +### 7.2 Preview App Listing + +Click **Preview Your App Listing Page** to see how your app appears in the marketplace. + +### 7.3 Share with Team Members + +To share your app with other users on your account: + +1. Go to **Authorization URL** section +2. Click **Generate** +3. Click **Copy** +4. Share the URL with your team members + +> **Note**: Beta apps can only be installed by members of the developer's Zoom account (security restriction). + +## Common Setup Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Bot JID not visible | Chatbot feature not enabled | Go to Features tab, toggle Chatbot ON | +| Can't find Secret Token | Team Chat Subscription not enabled | Enable Team Chat Subscription in Features → Surface | +| OAuth redirect error | Redirect URL not in allow list | Add full redirect URL to OAuth allow lists | +| Scopes not appearing | Wrong app type | Verify you created General App (OAuth), not S2S | +| App can't be added | Missing required configuration | Complete all steps in Basic Info and Features | + +## Verification Checklist + +Before proceeding to development, verify: + +- [ ] Created **General App (OAuth)** (not Server-to-Server) +- [ ] Selected appropriate App Management Type +- [ ] Configured OAuth redirect URL +- [ ] Added URLs to OAuth allow lists +- [ ] Enabled Team Chat in Surface tab (for chatbots) +- [ ] Configured Team Chat Subscription (for chatbots) +- [ ] Added all required scopes +- [ ] Obtained all required credentials +- [ ] Created .env file with credentials +- [ ] Successfully added app to your account + +## Next Steps + +### For Team Chat API: +1. [Authentication Flows](authentication.md) - Understand OAuth +2. [OAuth Setup Example](../examples/oauth-setup.md) - Implement OAuth +3. [Send Message Example](../examples/send-message.md) - Send first message + +### For Chatbot API: +1. [Webhook Architecture](webhooks.md) - Understand webhooks +2. [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your bot +3. [Message Cards Reference](../references/message-cards.md) - Create rich messages + +## Resources + +- [Zoom App Marketplace](https://marketplace.zoom.us/) +- [OAuth Documentation](https://developers.zoom.us/docs/integrations/oauth/) +- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/) +- [Using Role Management](https://support.zoom.us/hc/en-us/articles/115001078646) diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/message-structure.md b/partner-built/zoom-plugin/skills/team-chat/concepts/message-structure.md new file mode 100644 index 00000000..2a6c9892 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/message-structure.md @@ -0,0 +1,22 @@ +# Message Card Structure (Chatbot API) + +Chatbot messages use a card-like JSON structure (often called "message cards"). + +## High-Level Shape + +- `content.head`: title + optional subhead +- `content.body`: array of blocks + - `message` blocks for text + - `fields` blocks for key/value rows + - `actions` blocks for buttons + - `attachments` blocks for images/links + +## Where To Look + +- Component reference: `../references/message-cards.md` + +## Common Pitfalls + +- Buttons must include a `value` you can route on when you receive an interaction webhook. +- Many issues that look like "Zoom didn't render my card" are just invalid JSON shape; validate your payload before sending. + diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/security.md b/partner-built/zoom-plugin/skills/team-chat/concepts/security.md new file mode 100644 index 00000000..092d9855 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/security.md @@ -0,0 +1,18 @@ +# Security Best Practices + +## Webhooks + +- Verify webhook requests using Zoom’s verification mechanism for Team Chat subscriptions. +- Treat webhook payloads as untrusted input; validate fields before using them. + +## OAuth + +- Store refresh tokens securely (encrypt at rest). +- Rotate client secrets if they leak. +- Use least-privilege scopes. + +## Operational + +- Add rate limiting on your webhook endpoint. +- Log request IDs and correlation IDs (but avoid logging tokens / PII). + diff --git a/partner-built/zoom-plugin/skills/team-chat/concepts/webhooks.md b/partner-built/zoom-plugin/skills/team-chat/concepts/webhooks.md new file mode 100644 index 00000000..0b07f4ce --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/concepts/webhooks.md @@ -0,0 +1,493 @@ +# Webhook Architecture + +Complete guide to understanding and implementing Zoom Team Chat webhooks for interactive chatbots. + +## Overview + +Webhooks are HTTP POST requests that Zoom sends to your **Bot Endpoint URL** when specific events occur (slash commands, button clicks, form submissions, etc.). + +### How It Works + +``` +User action in Zoom → Zoom sends webhook → Your server processes → Send response +``` + +**Example flow**: +``` +1. User types "/weather San Francisco" in Zoom Team Chat +2. Zoom sends POST request to your Bot Endpoint URL +3. Your server receives webhook with payload.cmd = "San Francisco" +4. Your server calls weather API +5. Your server sends chatbot message back with weather data +``` + +## Webhook Lifecycle + +### Setup (One-time) + +1. **Configure Bot Endpoint URL** in Zoom Marketplace: + - Development: `https://abc123.ngrok.io/webhook` + - Production: `https://yourdomain.com/webhook` + +2. **Verify endpoint** - Zoom sends validation request when you save the URL + +### Runtime (Per Event) + +``` +User action → Zoom webhook → Your handler → Response +``` + +## Webhook Events + +| Event | Trigger | When It Fires | +|-------|---------|---------------| +| `endpoint.url_validation` | URL configured/changed | Setup only | +| `bot_installed` | Bot added to account | Installation | +| `bot_notification` | User messages bot or uses slash command | User interaction | +| `interactive_message_actions` | Button clicked | User clicks button | +| `chat_message.submit` | Form submitted | User submits form | +| `app_deauthorized` | Bot removed from account | Uninstallation | + +**See**: [Webhook Events Reference](../references/webhook-events.md) for complete event catalog + +## Webhook Structure + +### Request Headers + +Every webhook includes these headers: + +```javascript +{ + 'x-zm-signature': 'v0=abc123...', // Signature for verification + 'x-zm-request-timestamp': '1234567890', // Unix timestamp + 'content-type': 'application/json' +} +``` + +### Request Body + +```javascript +{ + "event": "bot_notification", // Event type + "payload": { // Event-specific data + "accountId": "...", + "toJid": "...", + "cmd": "...", + // ... more fields + } +} +``` + +## Webhook Verification + +**CRITICAL**: Always verify webhook signatures to prevent unauthorized requests. + +### Why Verify? + +Without verification, anyone can send fake webhooks to your endpoint, potentially: +- Triggering unauthorized actions +- Causing denial-of-service attacks +- Accessing sensitive data + +### Verification Algorithm + +```javascript +const crypto = require('crypto'); + +function verifyZoomWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const secretToken = process.env.ZOOM_VERIFICATION_TOKEN; + + if (!signature || !timestamp) { + throw new Error('Missing signature headers'); + } + + // Construct message + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + // Calculate expected signature + const expectedSignature = crypto + .createHmac('sha256', secretToken) + .update(message) + .digest('hex'); + + // Compare signatures + if (signature !== `v0=${expectedSignature}`) { + throw new Error('Invalid webhook signature'); + } + + return true; +} +``` + +### Verification Flow + +``` +1. Extract signature and timestamp from headers +2. Construct message: "v0:{timestamp}:{JSON body}" +3. Calculate HMAC-SHA256 with secret token +4. Compare calculated signature with header signature +5. Accept if match, reject if mismatch +``` + +## Webhook Handler Pattern + +### Basic Handler + +```javascript +app.post('/webhook', (req, res) => { + try { + // Step 1: Verify signature + verifyZoomWebhookSignature(req); + + // Step 2: Extract event and payload + const { event, payload } = req.body; + + // Step 3: Handle event + switch (event) { + case 'endpoint.url_validation': + return handleUrlValidation(req, res); + + case 'bot_installed': + return handleBotInstalled(payload, res); + + case 'bot_notification': + return handleBotNotification(payload, res); + + case 'interactive_message_actions': + return handleButtonClick(payload, res); + + case 'app_deauthorized': + return handleBotUninstalled(payload, res); + + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); + } + } catch (error) { + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid webhook signature' }); + } + return res.status(500).json({ error: error.message }); + } +}); +``` + +## Event Handlers + +### 1. URL Validation (`endpoint.url_validation`) + +Zoom sends this when you configure or change your Bot Endpoint URL. + +**Purpose**: Verify you control the endpoint + +**Payload**: +```javascript +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "xyz123abc" + } +} +``` + +**Required Response**: +```javascript +{ + "plainToken": "xyz123abc", + "encryptedToken": "hmac_sha256(plainToken, secret_token)" +} +``` + +**Implementation**: +```javascript +function handleUrlValidation(req, res) { + const { plainToken } = req.body.payload; + + const encryptedToken = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(plainToken) + .digest('hex'); + + return res.status(200).json({ + plainToken, + encryptedToken + }); +} +``` + +### 2. Bot Installed (`bot_installed`) + +Fired when someone adds your bot to their account. + +**Payload**: +```javascript +{ + "event": "bot_installed", + "payload": { + "accountId": "...", + "userId": "...", + "timestamp": 1234567890 + } +} +``` + +**Use Case**: Initialize bot state, send welcome message + +**Implementation**: +```javascript +async function handleBotInstalled(payload, res) { + console.log('Bot installed for account:', payload.accountId); + + // Optional: Initialize database, send welcome message + // await initializeBotForAccount(payload.accountId); + + return res.status(200).json({ success: true }); +} +``` + +### 3. Bot Notification (`bot_notification`) + +Fired when: +- User sends message to bot via slash command +- User sends direct message to bot + +**Payload**: +```javascript +{ + "event": "bot_notification", + "payload": { + "accountId": "...", + "toJid": "channel@conference.xmpp.zoom.us", + "robotJid": "bot@xmpp.zoom.us", + "userJid": "user@xmpp.zoom.us", + "cmd": "user's input text", + "userName": "John Doe", + "channelName": "Marketing", + "timestamp": 1234567890 + } +} +``` + +**Key Fields**: +- `cmd` - User's input after the slash command +- `toJid` - Where to send response (channel or DM) +- `accountId` - Account identifier + +**Use Case**: Process commands, integrate LLM, send responses + +**Implementation**: +```javascript +async function handleBotNotification(payload, res) { + const { toJid, cmd, accountId, userName } = payload; + + console.log(`${userName} sent: ${cmd}`); + + // Process command (e.g., call LLM) + const response = await processCommand(cmd); + + // Send response + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: response }] + }); + + return res.status(200).json({ success: true }); +} +``` + +### 4. Interactive Message Actions (`interactive_message_actions`) + +Fired when user clicks a button in a chatbot message. + +**Payload**: +```javascript +{ + "event": "interactive_message_actions", + "payload": { + "accountId": "...", + "toJid": "...", + "actionItem": { + "text": "Approve", + "value": "approve" // This is what you check + }, + "messageId": "...", + "userName": "John Doe" + } +} +``` + +**Key Field**: `actionItem.value` - The button's value you defined + +**Implementation**: +```javascript +async function handleButtonClick(payload, res) { + const { actionItem, toJid, accountId, userName } = payload; + + console.log(`${userName} clicked: ${actionItem.value}`); + + switch (actionItem.value) { + case 'approve': + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '✅ Approved!' }] + }); + break; + + case 'reject': + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '❌ Rejected' }] + }); + break; + + default: + console.log('Unknown action:', actionItem.value); + } + + return res.status(200).json({ success: true }); +} +``` + +## Webhook Best Practices + +### 1. Always Verify Signatures + +```javascript +// ✅ GOOD +app.post('/webhook', (req, res) => { + verifyZoomWebhookSignature(req); + // ... handle event +}); + +// ❌ BAD +app.post('/webhook', (req, res) => { + // No verification - vulnerable to fake webhooks! +}); +``` + +### 2. Respond Quickly + +Zoom expects a 200 response within 3 seconds. + +```javascript +// ✅ GOOD - Respond immediately, process async +app.post('/webhook', (req, res) => { + verifyZoomWebhookSignature(req); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process asynchronously + processWebhookAsync(req.body); +}); + +// ❌ BAD - Slow processing blocks response +app.post('/webhook', async (req, res) => { + await slowLLMCall(); // May timeout! + res.status(200).json({ success: true }); +}); +``` + +### 3. Handle All Events Gracefully + +```javascript +// ✅ GOOD - Handle unknown events +switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); +} + +// ❌ BAD - Crash on unknown events +switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + // Missing default case - crashes on new events! +} +``` + +### 4. Log Webhook Activity + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + console.log(`[Webhook] ${event}`, { + timestamp: new Date().toISOString(), + accountId: payload.accountId, + userId: payload.userId + }); + + // ... handle event +}); +``` + +### 5. Use Environment Variables + +```javascript +// ✅ GOOD +const SECRET_TOKEN = process.env.ZOOM_VERIFICATION_TOKEN; + +// ❌ BAD - Hardcoded secret +const SECRET_TOKEN = 'abc123xyz'; +``` + +## Testing Webhooks + +### Local Development with ngrok + +```bash +# Install ngrok +npm install -g ngrok + +# Expose local server +ngrok http 4000 + +# Copy HTTPS URL to Zoom Marketplace +# Example: https://abc123.ngrok.io/webhook +``` + +### Manual Testing + +```bash +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000" + +# Test with curl (will fail signature verification - expected) +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -d '{"event":"test"}' + +# Expected response: "Invalid webhook signature" (this is correct!) +``` + +### Verify Webhook is Working + +**Success indicators**: +1. Zoom successfully validates your endpoint URL +2. `bot_installed` event fires when you add the bot +3. `bot_notification` fires when you use slash command +4. Button clicks trigger `interactive_message_actions` + +## Common Webhook Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| "Cannot GET /webhook" | Browser sends GET, webhook is POST | Normal - test with POST or Zoom | +| "Invalid signature" | Wrong secret token | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace | +| URL validation fails | Response format incorrect | Return plainToken + encryptedToken | +| No webhooks received | Wrong endpoint URL | Verify URL in Zoom Marketplace matches your server | +| Webhooks timeout | Slow response | Return 200 immediately, process async | + +## Next Steps + +- [Webhook Events Reference](../references/webhook-events.md) - Complete event catalog +- [Button Actions Example](../examples/button-actions.md) - Handle button clicks +- [Slash Commands Example](../examples/slash-commands.md) - Process slash commands +- [LLM Integration Example](../examples/llm-integration.md) - Integrate Claude/GPT + +## Resources + +- [Chatbot Webhook Events](https://developers.zoom.us/docs/api/chatbot/events/) +- [Webhook Verification](https://developers.zoom.us/docs/api/webhooks/#verify-webhook-events) +- [Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/button-actions.md b/partner-built/zoom-plugin/skills/team-chat/examples/button-actions.md new file mode 100644 index 00000000..d0d9cdf3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/button-actions.md @@ -0,0 +1,18 @@ +# Button Actions (Chatbot API) + +Buttons in message cards send a webhook when clicked. + +## Pattern + +1. You send a card with `actions.items[]` where each button has a unique `value`. +2. Zoom sends `interactive_message_actions` to your webhook. +3. Your handler routes based on that `value`. + +## Routing Tip + +Use stable action IDs like: + +- `approve_request` +- `reject_request` +- `open_ticket:123` + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/channel-management.md b/partner-built/zoom-plugin/skills/team-chat/examples/channel-management.md new file mode 100644 index 00000000..1c4055c9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/channel-management.md @@ -0,0 +1,12 @@ +# Channel Management (Team Chat API) + +Typical use cases: + +- List channels a user can see (to let them pick a destination). +- Create channels (where supported by the API and account policy). + +## Pitfalls + +- Many "can't list channels" issues are missing `chat_channel:read`. +- Admin policies may prevent channel creation. + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/chatbot-setup.md b/partner-built/zoom-plugin/skills/team-chat/examples/chatbot-setup.md new file mode 100644 index 00000000..5d4914fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/chatbot-setup.md @@ -0,0 +1,520 @@ +# Chatbot Setup - Complete Working Example + +Build your first interactive Zoom chatbot from scratch. This guide provides complete, production-ready code. + +## Prerequisites + +- Completed [Environment Setup](../concepts/environment-setup.md) +- Obtained Bot JID, Client ID, Client Secret, Account ID, Secret Token +- Created .env file with credentials + +## Project Structure + +``` +my-zoom-chatbot/ +├── .env +├── .env.example +├── package.json +├── server.js +├── routes/ +│ └── webhook.js +└── utils/ + ├── auth.js + ├── chatbot.js + └── validation.js +``` + +## Step 1: Initialize Project + +```bash +mkdir my-zoom-chatbot +cd my-zoom-chatbot +npm init -y +``` + +## Step 2: Install Dependencies + +```bash +npm install express dotenv node-fetch +``` + +## Step 3: Create .env File + +```bash +# .env +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us +ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +``` + +## Step 4: Create Utility Files + +### utils/auth.js + +```javascript +// utils/auth.js +const fetch = require('node-fetch'); + +/** + * Get chatbot access token using client_credentials flow + */ +async function getChatbotToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).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=client_credentials' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Token error: ${error.error_description || error.error}`); + } + + const data = await response.json(); + return data.access_token; +} + +module.exports = { getChatbotToken }; +``` + +### utils/validation.js + +```javascript +// utils/validation.js +const crypto = require('crypto'); + +/** + * Verify Zoom webhook signature + */ +function verifyZoomWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!signature || !timestamp) { + throw new Error('Missing signature headers'); + } + + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + const hash = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(message) + .digest('hex'); + + if (signature !== `v0=${hash}`) { + throw new Error('Invalid webhook signature'); + } + + return true; +} + +/** + * Sanitize message (4096 char limit) + */ +function sanitizeMessage(message) { + if (typeof message !== 'string') return ''; + return message + .trim() + .replace(/[\x00-\x1F\x7F]/g, '') + .substring(0, 4096); +} + +/** + * Validate JID format + */ +function isValidJID(jid) { + if (typeof jid !== 'string' || !jid.trim()) return false; + return /^[^@\s]+@[^@\s]+$/.test(jid); +} + +module.exports = { + verifyZoomWebhookSignature, + sanitizeMessage, + isValidJID +}; +``` + +### utils/chatbot.js + +```javascript +// utils/chatbot.js +const fetch = require('node-fetch'); +const { getChatbotToken } = require('./auth'); +const { sanitizeMessage } = require('./validation'); + +/** + * Send chatbot message + */ +async function sendChatbotMessage(toJid, accountId, content) { + const accessToken = await getChatbotToken(); + + const body = { + robot_jid: process.env.ZOOM_BOT_JID, + to_jid: toJid, + account_id: accountId, + content: content + }; + + const response = await fetch('https://api.zoom.us/v2/im/chat/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Send message error: ${JSON.stringify(error)}`); + } + + return response.json(); +} + +/** + * Send simple text message + */ +async function sendTextMessage(toJid, accountId, text) { + return sendChatbotMessage(toJid, accountId, { + body: [ + { type: 'message', text: sanitizeMessage(text) } + ] + }); +} + +/** + * Send message with buttons + */ +async function sendMessageWithButtons(toJid, accountId, options) { + const { title, message, buttons } = options; + + return sendChatbotMessage(toJid, accountId, { + head: { + text: title + }, + body: [ + { type: 'message', text: sanitizeMessage(message) }, + { + type: 'actions', + items: buttons.map(btn => ({ + text: btn.text, + value: btn.value, + style: btn.style || 'Default' + })) + } + ] + }); +} + +/** + * Send message with fields + */ +async function sendMessageWithFields(toJid, accountId, options) { + const { title, fields } = options; + + return sendChatbotMessage(toJid, accountId, { + head: { + text: title + }, + body: [ + { + type: 'fields', + items: fields.map(field => ({ + key: field.key, + value: field.value + })) + } + ] + }); +} + +module.exports = { + sendChatbotMessage, + sendTextMessage, + sendMessageWithButtons, + sendMessageWithFields +}; +``` + +## Step 5: Create Webhook Handler + +### routes/webhook.js + +```javascript +// routes/webhook.js +const crypto = require('crypto'); +const { verifyZoomWebhookSignature } = require('../utils/validation'); +const { sendTextMessage, sendMessageWithButtons } = require('../utils/chatbot'); + +async function handleWebhook(req, res) { + try { + // Verify signature + verifyZoomWebhookSignature(req); + + const { event, payload } = req.body; + + switch (event) { + case 'endpoint.url_validation': + return handleUrlValidation(req, res); + + case 'bot_installed': + console.log('Bot installed for account:', payload.accountId); + return res.status(200).json({ success: true }); + + case 'bot_notification': + return handleBotNotification(payload, res); + + case 'interactive_message_actions': + return handleButtonClick(payload, res); + + case 'app_deauthorized': + console.log('Bot uninstalled for account:', payload.accountId); + return res.status(200).json({ success: true }); + + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); + } + } catch (error) { + console.error('Webhook error:', error); + + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid webhook signature' }); + } + + return res.status(500).json({ error: error.message }); + } +} + +/** + * Handle URL validation + */ +function handleUrlValidation(req, res) { + const { plainToken } = req.body.payload; + + const encryptedToken = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(plainToken) + .digest('hex'); + + return res.status(200).json({ + plainToken, + encryptedToken + }); +} + +/** + * Handle bot notification (slash command or direct message) + */ +async function handleBotNotification(payload, res) { + const { toJid, cmd, accountId, userName } = payload; + + console.log(`${userName} sent: ${cmd}`); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process command asynchronously + try { + // Simple command router + if (cmd.toLowerCase().includes('help')) { + await sendTextMessage(toJid, accountId, + 'Available commands:\n- help: Show this message\n- ping: Test bot\n- demo: Show demo buttons' + ); + } + else if (cmd.toLowerCase().includes('ping')) { + await sendTextMessage(toJid, accountId, 'Pong! 🏓'); + } + else if (cmd.toLowerCase().includes('demo')) { + await sendMessageWithButtons(toJid, accountId, { + title: 'Demo Buttons', + message: 'Click a button below:', + buttons: [ + { text: 'Option A', value: 'option_a', style: 'Primary' }, + { text: 'Option B', value: 'option_b', style: 'Default' }, + { text: 'Cancel', value: 'cancel', style: 'Danger' } + ] + }); + } + else { + await sendTextMessage(toJid, accountId, + `You said: "${cmd}"\n\nType "help" to see available commands.` + ); + } + } catch (error) { + console.error('Error processing command:', error); + } +} + +/** + * Handle button click + */ +async function handleButtonClick(payload, res) { + const { actionItem, toJid, accountId, userName } = payload; + + console.log(`${userName} clicked: ${actionItem.value}`); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process button click asynchronously + try { + switch (actionItem.value) { + case 'option_a': + await sendTextMessage(toJid, accountId, '✅ You selected Option A'); + break; + + case 'option_b': + await sendTextMessage(toJid, accountId, '✅ You selected Option B'); + break; + + case 'cancel': + await sendTextMessage(toJid, accountId, '❌ Cancelled'); + break; + + default: + await sendTextMessage(toJid, accountId, `Unknown action: ${actionItem.value}`); + } + } catch (error) { + console.error('Error processing button click:', error); + } +} + +module.exports = { handleWebhook }; +``` + +## Step 6: Create Main Server + +### server.js + +```javascript +// server.js +require('dotenv').config(); +const express = require('express'); +const { handleWebhook } = require('./routes/webhook'); + +const app = express(); +const PORT = process.env.PORT || 4000; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Routes +app.get('/', (req, res) => { + res.json({ message: 'Zoom Team Chat Bot is running!' }); +}); + +app.post('/webhook', handleWebhook); + +// Start server +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Webhook endpoint: ${process.env.PUBLIC_BASE_URL || 'https://YOUR_PUBLIC_BASE_URL'}/webhook`); +}); +``` + +## Step 7: Test Locally with ngrok + +```bash +# Install ngrok +npm install -g ngrok + +# Start your server +node server.js + +# In a new terminal, expose with ngrok +ngrok http 4000 + +# Copy the HTTPS URL (e.g., https://abc123.ngrok.io) +``` + +## Step 8: Configure Zoom Marketplace + +1. Go to your app in [Zoom Marketplace](https://marketplace.zoom.us/) +2. Navigate to **Features** → **Team Chat Subscription** +3. Set **Bot Endpoint URL**: `https://abc123.ngrok.io/webhook` +4. Set **Slash Command**: `/mybot` +5. Click **Save** + +Zoom will send a `endpoint.url_validation` request. If successful, you'll see a green checkmark. + +## Step 9: Install and Test + +1. Go to **Local Test** page in Zoom Marketplace +2. Click **Add App Now** +3. Click **Allow** +4. Open Zoom Team Chat +5. In any channel, type: `/mybot help` + +You should see the bot respond with the help message! + +## Testing Checklist + +- [ ] `/mybot help` - Shows help message +- [ ] `/mybot ping` - Responds with "Pong! 🏓" +- [ ] `/mybot demo` - Shows buttons +- [ ] Click button - Sends confirmation message + +## Production Deployment + +### Environment Variables + +```bash +# Production .env +ZOOM_CLIENT_ID=your_production_client_id +ZOOM_CLIENT_SECRET=your_production_client_secret +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us # Production Bot JID +ZOOM_VERIFICATION_TOKEN=your_production_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +NODE_ENV=production +``` + +### Deploy to Cloud + +**Options**: +- Heroku +- AWS Lambda +- Google Cloud Run +- Digital Ocean App Platform +- Vercel (with serverless functions) + +**Requirements**: +- HTTPS endpoint (required for production) +- Publicly accessible URL +- Update Bot Endpoint URL in Zoom Marketplace to production URL + +## Next Steps + +- [Button Actions](button-actions.md) - Advanced button handling +- [LLM Integration](llm-integration.md) - Add Claude/GPT +- [Message Cards Reference](../references/message-cards.md) - Rich message components +- [Webhook Events Reference](../references/webhook-events.md) - All webhook events + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| "Invalid signature" | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace | +| Bot doesn't respond | Check ngrok is running and URL is correct | +| URL validation fails | Ensure endpoint returns plainToken + encryptedToken | +| Messages not sending | Verify Bot JID and Account ID are correct | + +## Resources + +- [Chatbot Quickstart (Official)](https://github.com/zoom/chatbot-nodejs-quickstart) +- [Claude Chatbot Sample](https://github.com/zoom/zoom-chatbot-claude-sample) +- [Unsplash Chatbot Sample](https://github.com/zoom/unsplash-chatbot) diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/database-integration.md b/partner-built/zoom-plugin/skills/team-chat/examples/database-integration.md new file mode 100644 index 00000000..ae04f16e --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/database-integration.md @@ -0,0 +1,14 @@ +# Database Integration (Stateful Bots) + +Store state when you need: + +- multi-step workflows +- approvals +- linking Zoom users to internal system users + +## Suggested Tables + +- `installations` (account_id, bot_jid, created_at) +- `users` (zoom_jid, internal_user_id) +- `workflows` (workflow_id, status, payload_json) + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/dropdown-selects.md b/partner-built/zoom-plugin/skills/team-chat/examples/dropdown-selects.md new file mode 100644 index 00000000..c9690fd3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/dropdown-selects.md @@ -0,0 +1,14 @@ +# Dropdown Selects (Chatbot API) + +Dropdowns can be used for: + +- picking a channel +- selecting a user +- selecting from a fixed list + +## Pattern + +1. Send a card with a select/dropdown element. +2. User selects an option and submits (or triggers an action). +3. Handle selection via webhook and update state. + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/form-submissions.md b/partner-built/zoom-plugin/skills/team-chat/examples/form-submissions.md new file mode 100644 index 00000000..e718031a --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/form-submissions.md @@ -0,0 +1,15 @@ +# Form Submissions (Chatbot API) + +Forms inside cards can collect user input; submissions arrive via webhook. + +## Pattern + +1. Send a card with form fields. +2. Receive `chat_message.submit` webhook. +3. Validate inputs and respond with an updated card or confirmation message. + +## Pitfalls + +- Always validate types (dates, numbers) server-side. +- Treat submitted text as untrusted input. + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/llm-integration.md b/partner-built/zoom-plugin/skills/team-chat/examples/llm-integration.md new file mode 100644 index 00000000..dcd7cdf7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/llm-integration.md @@ -0,0 +1,16 @@ +# LLM Integration + +Use an LLM to interpret user intent from Team Chat messages, then call Zoom APIs or respond with rich message cards. + +Recommended flow: + +1. Receive `bot_notification` event. +2. Extract user text and channel context. +3. Classify intent with your LLM (meeting actions, help, status). +4. Execute safe backend actions (for example, create/list meetings). +5. Send structured response back to Team Chat. + +Implementation references: + +- [Chatbot Setup](chatbot-setup.md) +- [Sample Repositories](../references/samples.md) diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/multi-step-workflows.md b/partner-built/zoom-plugin/skills/team-chat/examples/multi-step-workflows.md new file mode 100644 index 00000000..05ea8e08 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/multi-step-workflows.md @@ -0,0 +1,13 @@ +# Multi-Step Workflows (Chatbot API) + +## Pattern + +1. Send a card with buttons (step 1). +2. On click, update stored state and respond with step 2 card. +3. Repeat until completion. + +## Pitfalls + +- Webhooks can be delivered more than once; de-dupe by event ID if available. +- Avoid storing PII in logs. + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/oauth-setup.md b/partner-built/zoom-plugin/skills/team-chat/examples/oauth-setup.md new file mode 100644 index 00000000..618a9bac --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/oauth-setup.md @@ -0,0 +1,49 @@ +# OAuth Setup (Team Chat API) + +This is for the **Team Chat API** (user-level actions). + +## What You Need + +- App type: **General App (OAuth)** +- Redirect URL: your app's callback URL +- Scopes (typical): + - `chat_message:write` + - `chat_channel:read` + +## Flow Summary + +1. Redirect user to Zoom authorize URL. +2. Receive `code` at your redirect URL. +3. Exchange `code` for `access_token` + `refresh_token`. +4. Store tokens per-user. +5. Refresh the access token when it expires. + +## In-App Web Flow Pattern (Recommended) + +For browser demos, keep the whole flow in your app to avoid manual copy/paste mistakes: + +1. User clicks **Connect Zoom User** in your UI. +2. Backend returns authorize URL (`https://zoom.us/oauth/authorize`) with a generated `state`. +3. Redirect browser to Zoom consent screen. +4. Callback route validates `state` and exchanges `code` at `https://zoom.us/oauth/token`. +5. Callback page stores token in app storage (for demo: localStorage, for production: server session/DB) and redirects back to app. + +## Token Exchange (Server Side) + +Pseudo-code (Node style): + +```js +// POST https://zoom.us/oauth/token +// grant_type=authorization_code&code=...&redirect_uri=... +// Authorization: Basic base64(client_id:client_secret) +``` + +## Common Errors + +- `Invalid redirect`: redirect URL mismatch between code exchange and Marketplace config. +- `Invalid access token, does not contain scopes`: missing scopes on the app or user didn't re-consent after scope change. + +## Next + +- `send-message.md` to post a message once you have a user token. +- `token-management.md` for refresh strategy. diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/scheduled-alerts.md b/partner-built/zoom-plugin/skills/team-chat/examples/scheduled-alerts.md new file mode 100644 index 00000000..0b30e76c --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/scheduled-alerts.md @@ -0,0 +1,14 @@ +# Scheduled Alerts (Team Chat) + +## Two Common Approaches + +1. Team Chat API (as user): + - Cron triggers, refresh user token, send message to a channel. +2. Chatbot API (as bot): + - Cron triggers, request bot token, send message card. + +## Pitfalls + +- Don’t store tokens in plaintext. +- Ensure your cron job is idempotent (avoid duplicate messages). + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/send-message.md b/partner-built/zoom-plugin/skills/team-chat/examples/send-message.md new file mode 100644 index 00000000..d408cdd9 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/send-message.md @@ -0,0 +1,23 @@ +# Send Your First Message (Team Chat API) + +This is for **Team Chat API** (messages sent as the authenticated user). + +## Endpoint + +`POST https://api.zoom.us/v2/chat/users/me/messages` + +## Minimal Payload + +```json +{ + "message": "Hello from my integration", + "to_channel": "CHANNEL_ID" +} +``` + +## Common Pitfalls + +- `to_channel` must be a channel the user can access. +- Use the correct scopes: + - `chat_message:write` + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/slash-commands.md b/partner-built/zoom-plugin/skills/team-chat/examples/slash-commands.md new file mode 100644 index 00000000..c56d41ad --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/slash-commands.md @@ -0,0 +1,16 @@ +# Slash Commands (Chatbot API) + +Slash commands are configured on the Marketplace app and trigger webhook events. + +## Pattern + +1. Configure `/yourcommand` in the Chatbot feature settings. +2. User runs the command in Team Chat. +3. Your webhook receives `bot_notification` (or equivalent) with the command text. +4. Parse args and respond with a message card. + +## Pitfalls + +- Commands are account-scoped; make sure you're testing in the right account. +- Don’t rely on client-side parsing; parse on your server. + diff --git a/partner-built/zoom-plugin/skills/team-chat/examples/token-management.md b/partner-built/zoom-plugin/skills/team-chat/examples/token-management.md new file mode 100644 index 00000000..a5e3506a --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/examples/token-management.md @@ -0,0 +1,20 @@ +# Token Management (Team Chat API) + +## Storage + +Store per-user: + +- `access_token` +- `refresh_token` +- `expires_at` (absolute timestamp) + +## Refresh Strategy + +- Refresh "just-in-time" when an API call fails with token expiry, or +- Refresh proactively when `now >= expires_at - 60s`. + +## Pitfalls + +- Refresh tokens can expire or be revoked (user removes app, admin blocks app). +- When you change scopes, existing users may need to reauthorize. + diff --git a/partner-built/zoom-plugin/skills/team-chat/get-started.md b/partner-built/zoom-plugin/skills/team-chat/get-started.md new file mode 100644 index 00000000..c6bb74e4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/get-started.md @@ -0,0 +1,60 @@ +# Team Chat Get Started + +This is the fast path for Zoom Team Chat integrations. + +## Step 1: Pick Integration Type First + +- **User type** (Team Chat API) + - Auth: `authorization_code` (User OAuth) + - Endpoint family: `/v2/chat/users/...` + - Messages appear as user + +- **Bot type** (Chatbot API) + - Auth: `client_credentials` + - Endpoint family: `/v2/im/chat/messages` + - Messages appear as bot + +If this decision is wrong, auth/scopes/endpoints will all mismatch. + +## Step 2: Set Up App + Credentials + +1. Create **General App (OAuth)** in Zoom Marketplace. +2. Configure scopes and feature settings. +3. Gather credentials from app config: + - `ZOOM_CLIENT_ID` + - `ZOOM_CLIENT_SECRET` + - `ZOOM_BOT_JID` (bot type) + - `ZOOM_ACCOUNT_ID` (bot type use cases) + +See: `concepts/environment-setup.md` + +## Step 3A: User Type (Team Chat API) + +1. Implement OAuth code flow. +2. Call `POST /v2/chat/users/me/messages` with bearer token. +3. Use OAuth endpoints correctly: + - authorize: `https://zoom.us/oauth/authorize` + - token exchange: `https://zoom.us/oauth/token` + +See: +- `examples/oauth-setup.md` +- `examples/send-message.md` + +## Step 3B: Bot Type (Chatbot API) + +1. Get token via `grant_type=client_credentials`. +2. Call `POST /v2/im/chat/messages`. +3. Add webhook endpoint for interactive events. +4. Use `https://zoom.us/oauth/token` for `client_credentials` token requests. + +See: +- `examples/chatbot-setup.md` +- `concepts/webhooks.md` +- `references/message-cards.md` + +## Step 4: Validate with a Minimal Smoke Test + +- User type: send one plain text channel message. +- Bot type: send one plain text bot message. + +Then add advanced features (buttons/forms/slash commands). diff --git a/partner-built/zoom-plugin/skills/team-chat/references/api-reference.md b/partner-built/zoom-plugin/skills/team-chat/references/api-reference.md new file mode 100644 index 00000000..4ed8dc81 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/api-reference.md @@ -0,0 +1,23 @@ +# API Reference Pointers + +This doc is intentionally lightweight; prefer the official REST reference for the authoritative schema. + +## Team Chat API (user-level) + +- Send message: `POST /v2/chat/users/me/messages` +- Typical needs: + - list channels + - post to channel / DM + - thread replies + +## Chatbot API (bot-level) + +- Send bot message: `POST /v2/im/chat/messages` + +## Notes + +- If you see "invalid access token" errors, check: + - app type (General App OAuth vs others) + - scopes + - whether the user re-consented after scope changes + diff --git a/partner-built/zoom-plugin/skills/team-chat/references/environment-variables.md b/partner-built/zoom-plugin/skills/team-chat/references/environment-variables.md new file mode 100644 index 00000000..034ee706 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/environment-variables.md @@ -0,0 +1,21 @@ +# Zoom Team Chat Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | Team Chat app OAuth identity | Zoom Marketplace -> Team Chat app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> Team Chat app -> App Credentials | +| `ZOOM_REDIRECT_URI` | OAuth code flow | Callback URL for installs/auth | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_BOT_JID` | Chatbot flows | Target bot identifier | Team Chat app/chatbot configuration after setup | +| `ZOOM_SECRET_TOKEN` | Recommended | Event/webhook signature verification | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `ZOOM_VERIFICATION_TOKEN` | Legacy only | Legacy verification path | Zoom Marketplace legacy fields (older apps) | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +## Notes + +- Prefer secret-token signature verification over legacy verification token. diff --git a/partner-built/zoom-plugin/skills/team-chat/references/error-codes.md b/partner-built/zoom-plugin/skills/team-chat/references/error-codes.md new file mode 100644 index 00000000..b8731b0d --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/error-codes.md @@ -0,0 +1,22 @@ +# Error Codes (Common Patterns) + +## Auth Errors + +- `Invalid access token` + - wrong token type (bot token used for user API, or vice versa) + - missing scopes + - token expired / revoked + +## Webhook Errors + +- No events received: + - endpoint not reachable publicly + - verification failing + - wrong event subscription / wrong app/account + +## Message Rendering Issues + +- Card not rendering: + - invalid JSON payload + - unsupported component types + diff --git a/partner-built/zoom-plugin/skills/team-chat/references/jid-formats.md b/partner-built/zoom-plugin/skills/team-chat/references/jid-formats.md new file mode 100644 index 00000000..1013df3e --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/jid-formats.md @@ -0,0 +1,10 @@ +# JID Formats (Quick Guide) + +JIDs identify users/bots in Team Chat contexts. + +## Practical Tips + +- Treat JIDs as opaque identifiers. +- Store them exactly as received. +- Don’t parse structure unless Zoom explicitly documents the format you need. + diff --git a/partner-built/zoom-plugin/skills/team-chat/references/message-cards.md b/partner-built/zoom-plugin/skills/team-chat/references/message-cards.md new file mode 100644 index 00000000..f14219a8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/message-cards.md @@ -0,0 +1,343 @@ +# Message Card Components Reference + +Complete reference for building rich interactive messages in Zoom Team Chat chatbots. + +## Card Structure + +Every chatbot message has this structure: + +```javascript +{ + "content": { + "head": { // Optional header + "text": "Title", + "sub_head": { "text": "Subtitle" } + }, + "body": [ // Array of components + { "type": "message", "text": "Content" }, + { "type": "actions", "items": [...] } + // ... more components + ] + } +} +``` + +## Components Catalog + +### Text Components + +#### message +Plain text content. + +```javascript +{ + "type": "message", + "text": "Hello, this is plain text" +} +``` + +#### header +Title text with optional styling. + +```javascript +{ + "type": "header", + "text": "Main Heading", + "style": { + "bold": true, + "italic": false + } +} +``` + +#### styled_text +Text with markdown-like styling. + +```javascript +{ + "type": "styled_text", + "text": "**Bold** *italic* `code`" +} +``` + +### Interactive Components + +#### actions (Buttons) +Clickable buttons that trigger webhooks. + +```javascript +{ + "type": "actions", + "items": [ + { + "text": "Approve", + "value": "approve", + "style": "Primary" // Primary, Danger, Default + }, + { + "text": "Reject", + "value": "reject", + "style": "Danger" + } + ] +} +``` + +**Styles**: +- `Primary` - Blue button +- `Danger` - Red button +- `Default` - Gray button + +#### dropdown +Select menu with options. + +```javascript +{ + "type": "dropdown", + "select_items": [ + { "text": "Option 1", "value": "opt1" }, + { "text": "Option 2", "value": "opt2" } + ] +} +``` + +#### form_field +Text input field. + +```javascript +{ + "type": "form_field", + "editable": true, + "text": "Enter your name" +} +``` + +### Layout Components + +#### section +Group components with optional colored sidebar. + +```javascript +{ + "type": "section", + "sidebar_color": "#3b82f6", // Hex color + "sections": [ + { "type": "message", "text": "Grouped content" } + ] +} +``` + +**Common colors**: +- Success: `#10b981` (green) +- Error: `#ef4444` (red) +- Warning: `#f59e0b` (orange) +- Info: `#3b82f6` (blue) + +#### fields +Key-value pairs displayed in columns. + +```javascript +{ + "type": "fields", + "items": [ + { "key": "Status", "value": "Active" }, + { "key": "Priority", "value": "High" }, + { "key": "Assignee", "value": "John Doe" } + ] +} +``` + +#### divider +Horizontal line separator. + +```javascript +{ + "type": "divider" +} +``` + +### Media Components + +#### attachments +Image with optional link. + +```javascript +{ + "type": "attachments", + "img_url": "https://example.com/image.jpg", + "resource_url": "https://example.com/full-page", + "information": { + "title": { "text": "Image Title" }, + "description": { "text": "Click to view" } + } +} +``` + +## Complete Examples + +### Build Notification + +```javascript +{ + "content": { + "head": { + "text": "Build #123 Complete", + "sub_head": { "text": "main branch" } + }, + "body": [ + { + "type": "section", + "sidebar_color": "#10b981", + "sections": [ + { "type": "message", "text": "✅ Build completed successfully" } + ] + }, + { + "type": "fields", + "items": [ + { "key": "Branch", "value": "main" }, + { "key": "Commit", "value": "abc123" }, + { "key": "Duration", "value": "2m 34s" } + ] + }, + { + "type": "actions", + "items": [ + { "text": "View Logs", "value": "view_logs", "style": "Primary" }, + { "text": "Deploy", "value": "deploy", "style": "Default" } + ] + } + ] + } +} +``` + +### Approval Request + +```javascript +{ + "content": { + "head": { + "text": "Expense Approval Required" + }, + "body": [ + { "type": "message", "text": "John Doe submitted an expense report" }, + { + "type": "fields", + "items": [ + { "key": "Amount", "value": "$500.00" }, + { "key": "Category", "value": "Travel" }, + { "key": "Date", "value": "Feb 9, 2026" } + ] + }, + { "type": "divider" }, + { + "type": "actions", + "items": [ + { "text": "Approve", "value": "approve_500", "style": "Primary" }, + { "text": "Reject", "value": "reject_500", "style": "Danger" }, + { "text": "View Details", "value": "details_500", "style": "Default" } + ] + } + ] + } +} +``` + +### Error Notification + +```javascript +{ + "content": { + "head": { + "text": "⚠️ Service Alert" + }, + "body": [ + { + "type": "section", + "sidebar_color": "#ef4444", + "sections": [ + { "type": "message", "text": "Database connection failed" } + ] + }, + { + "type": "fields", + "items": [ + { "key": "Service", "value": "api-prod" }, + { "key": "Error", "value": "Connection timeout" }, + { "key": "Time", "value": "2026-02-09 18:30:00 UTC" } + ] + }, + { + "type": "actions", + "items": [ + { "text": "View Logs", "value": "logs", "style": "Primary" }, + { "text": "Acknowledge", "value": "ack", "style": "Default" } + ] + } + ] + } +} +``` + +## Limitations + +| Component | Limit | +|-----------|-------| +| Message text | 4,096 characters | +| Button text | 40 characters | +| Field key/value | 256 characters each | +| Dropdown options | 100 options | +| Buttons per message | 5 buttons | + +## Best Practices + +### Button Design +✅ **DO**: Use clear, action-oriented labels +- "Approve Request" +- "View Details" +- "Cancel Order" + +❌ **DON'T**: Use vague labels +- "OK" +- "Click Here" +- "Button" + +### Color Usage +✅ **DO**: Use semantic colors +- Green (`#10b981`) for success +- Red (`#ef4444`) for errors/destructive actions +- Blue (`#3b82f6`) for info +- Orange (`#f59e0b`) for warnings + +❌ **DON'T**: Use random colors without meaning + +### Field Formatting +✅ **DO**: Keep keys concise, values informative +```javascript +{ "key": "Status", "value": "Active" } +``` + +❌ **DON'T**: Make keys too long +```javascript +{ "key": "The current status of the request", "value": "Active" } +``` + +## Testing Cards + +Use the [Team Chat App Card Builder](https://appssdk.zoom.us/cardbuilder/) to: +- Preview card designs +- Test layouts +- Generate JSON + +## Next Steps + +- [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot +- [Button Actions](../examples/button-actions.md) - Handle button clicks +- [Webhook Events](webhook-events.md) - Understand webhook payloads + +## Resources + +- [Official Card Components](https://developers.zoom.us/docs/team-chat/customizing-messages/) +- [App Card Builder](https://appssdk.zoom.us/cardbuilder/) +- [Sample Chatbots](https://github.com/zoom?q=chatbot) diff --git a/partner-built/zoom-plugin/skills/team-chat/references/rate-limits.md b/partner-built/zoom-plugin/skills/team-chat/references/rate-limits.md new file mode 100644 index 00000000..4e340c94 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/rate-limits.md @@ -0,0 +1,8 @@ +# Rate Limits + +Rate limits vary by endpoint and account. If you get throttled: + +- add retries with exponential backoff +- batch work where possible +- avoid calling list endpoints repeatedly (cache results) + diff --git a/partner-built/zoom-plugin/skills/team-chat/references/sample-comparison.md b/partner-built/zoom-plugin/skills/team-chat/references/sample-comparison.md new file mode 100644 index 00000000..e9d35895 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/sample-comparison.md @@ -0,0 +1,18 @@ +# Sample Comparison + +Use this quick matrix to choose the right Team Chat or chatbot sample shape before building your own integration. + +| Sample shape | Best when | Strengths | Watch-outs | +|--------------|-----------|-----------|------------| +| Minimal webhook bot | You want to validate event flow quickly | Fastest setup, easy signature verification review | Usually in-memory state only | +| Full chatbot sample | You need slash commands, cards, and bot responses together | Shows end-to-end chat lifecycle | More moving parts than a simple receiver | +| LLM-enhanced bot | You want summarization or assistant behavior | Good reference for prompt assembly and response shaping | Requires stricter latency and fallback design | +| Multi-language sample | You need parity across runtimes | Useful for comparing auth and verification patterns | Feature coverage often drifts between languages | + +## What to Compare + +- OAuth flow type and token handling +- Webhook signature verification approach +- Message card rendering support +- Persistence model: in-memory, file, or database +- Local development strategy: tunnel, mock events, replay fixtures diff --git a/partner-built/zoom-plugin/skills/team-chat/references/samples.md b/partner-built/zoom-plugin/skills/team-chat/references/samples.md new file mode 100644 index 00000000..4a8eb834 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/samples.md @@ -0,0 +1,547 @@ +# Sample Applications Analysis + +Analysis of 10 official Zoom Team Chat sample applications, extracted patterns, and best practices. + +## Sample Overview + +| Sample | Language | Complexity | Best For | +|--------|----------|------------|----------| +| [chatbot-nodejs-quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) | Node.js | ⭐ Beginner | **Start here** - Tutorial series | +| [zoom-chatbot-claude-sample](https://github.com/zoom/zoom-chatbot-claude-sample) | Node.js | ⭐⭐ Intermediate | LLM integration pattern | +| [unsplash-chatbot](https://github.com/zoom/unsplash-chatbot) | Node.js | ⭐⭐ Intermediate | API integration + database | +| [zoom-erp-chatbot-sample](https://github.com/zoom/zoom-erp-chatbot-sample) | Node.js | ⭐⭐⭐ Advanced | Enterprise integration | +| [task-manager-sample](https://github.com/zoom/task-manager-sample) | Node.js | ⭐⭐⭐ Advanced | Full CRUD application | +| [zoom-cohere-chatbot-sample](https://github.com/zoom/zoom-cohere-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cohere LLM integration | +| [zoom-cerebras-chatbot-sample](https://github.com/zoom/zoom-cerebras-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cerebras LLM integration | +| [zoom-team-chat-shortcut-sample](https://github.com/zoom/zoom-team-chat-shortcut-sample) | Node.js | ⭐⭐ Intermediate | Shortcuts and UI elements | +| [zoom-teams-chat-snowflake-sample](https://github.com/zoom/zoom-teams-chat-snowflake-sample) | Node.js | ⭐⭐⭐ Advanced | Snowflake data integration | +| [rivet-javascript-sample](https://github.com/zoom/rivet-javascript-sample) | Node.js | ⭐⭐ Intermediate | Rivet SDK usage | + +## 1. chatbot-nodejs-quickstart + +**Repository**: https://github.com/zoom/chatbot-nodejs-quickstart + +**Description**: Official tutorial series covering 9 episodes from setup to advanced features. + +**Key Features**: +- Setup & Send Messages +- Handle Events +- Slash Commands +- Markdown & Emojis +- Reactions & Interactive Messages +- Threaded Replies +- Search Messages via API +- Scheduling Messages +- Zoom Workplace App Integration + +**Project Structure**: +``` +chatbot-nodejs-quickstart/ +├── routes/ +│ ├── zoom-webhookHandler.js # Webhook event handling +│ └── oauth-routes.js # OAuth flow +├── utils/ +│ ├── zoom-api.js # API helper functions +│ ├── zoom-chatbot-auth.js # Token generation +│ └── validation.js # Webhook signature verification +├── views/ # EJS templates +├── server.js # Express app +└── .env.example # Environment variables +``` + +**Key Patterns**: + +### Webhook Handler Pattern +```javascript +async function handleZoomWebhook(req, res) { + verifyZoomWebhookSignature(req); + + const { event, payload } = req.body; + + switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + case 'interactive_message_actions': + return handleButtonClick(payload, res); + // ... more cases + } +} +``` + +### Token Generation +```javascript +async function getChatbotToken() { + const credentials = Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64'); + + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { 'Authorization': `Basic ${credentials}` }, + body: 'grant_type=client_credentials' + }); + + return (await response.json()).access_token; +} +``` + +**Best Practices**: +- ✅ Signature verification on all webhooks +- ✅ Environment variables for credentials +- ✅ Modular route structure +- ✅ Error handling with try/catch +- ✅ Immediate webhook response (200 status) + +**Recommended For**: First-time chatbot developers + +## 2. zoom-chatbot-claude-sample + +**Repository**: https://github.com/zoom/zoom-chatbot-claude-sample + +**Description**: AI-powered chatbot using Anthropic Claude for natural language responses. + +**Key Features**: +- Claude API integration +- Conversation history tracking +- Streaming responses (optional) +- Context management + +**LLM Integration Pattern**: +```javascript +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // Call Claude API + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1024, + messages: [{ role: 'user', content: cmd }] + }); + + const llmResponse = response.content[0].text; + + // Send back to Zoom + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: llmResponse }] + }); +} +``` + +**Conversation History Pattern**: +```javascript +const conversationHistory = new Map(); + +function addToHistory(userId, role, content) { + if (!conversationHistory.has(userId)) { + conversationHistory.set(userId, []); + } + conversationHistory.get(userId).push({ role, content }); +} + +// In bot_notification handler +const history = conversationHistory.get(userId) || []; +const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-20250514', + messages: history +}); +``` + +**Environment Variables**: +```bash +ANTHROPIC_API_KEY=your_api_key_here +ZOOM_CLIENT_ID=... +ZOOM_CLIENT_SECRET=... +ZOOM_BOT_JID=... +``` + +**Recommended For**: Building AI assistants + +## 3. unsplash-chatbot + +**Repository**: https://github.com/zoom/unsplash-chatbot + +**Description**: Image search bot integrating Unsplash API with database storage. + +**Key Features**: +- Third-party API integration (Unsplash) +- Database persistence (SQLite/PostgreSQL) +- Image search and display +- User preference storage + +**Database Schema**: +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + zoom_user_id TEXT UNIQUE, + preferences TEXT +); + +CREATE TABLE searches ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + query TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +**Image Display Pattern**: +```javascript +{ + "content": { + "head": { "text": "Image Results" }, + "body": [ + { + "type": "attachments", + "img_url": imageData.urls.regular, + "resource_url": imageData.links.html, + "information": { + "title": { "text": imageData.description }, + "description": { "text": `Photo by ${imageData.user.name}` } + } + } + ] + } +} +``` + +**Best Practices**: +- ✅ API rate limiting handling +- ✅ Error handling for external APIs +- ✅ Database connection pooling +- ✅ User data privacy + +**Recommended For**: External API integration patterns + +## 4. zoom-erp-chatbot-sample + +**Repository**: https://github.com/zoom/zoom-erp-chatbot-sample + +**Description**: Enterprise Resource Planning integration with scheduled alerts. + +**Key Features**: +- Oracle ERP API integration +- Scheduled notifications (cron) +- Approval workflows +- Threaded conversations + +**Scheduled Alerts Pattern**: +```javascript +const cron = require('node-cron'); + +// Daily report at 9 AM +cron.schedule('0 9 * * *', async () => { + const report = await getERPReport(); + + await sendChatbotMessage(channelJid, accountId, { + head: { "text": "Daily ERP Report" }, + body: [ + { "type": "fields", "items": report.fields }, + { + "type": "actions", + "items": [ + { "text": "View Details", "value": "view_report" } + ] + } + ] + }); +}); +``` + +**Approval Workflow Pattern**: +```javascript +// Send approval request +{ + "head": { "text": "Expense Approval Required" }, + "body": [ + { "type": "fields", "items": expenseFields }, + { + "type": "actions", + "items": [ + { "text": "Approve", "value": `approve_${expenseId}`, "style": "Primary" }, + { "text": "Reject", "value": `reject_${expenseId}`, "style": "Danger" } + ] + } + ] +} + +// Handle button click +case 'interactive_message_actions': { + const action = payload.actionItem.value; + const [decision, expenseId] = action.split('_'); + + await updateERPStatus(expenseId, decision); + await sendConfirmation(payload.toJid, decision); +} +``` + +**Recommended For**: Enterprise integrations, workflows + +## 5. task-manager-sample + +**Repository**: https://github.com/zoom/task-manager-sample + +**Description**: Full-featured task management application with CRUD operations. + +**Key Features**: +- Create, read, update, delete tasks +- Task assignment +- Due date tracking +- Status management +- Persistent storage + +**CRUD Pattern**: +```javascript +// CREATE +case 'bot_notification': { + if (cmd.startsWith('create task')) { + const taskData = parseTaskCommand(cmd); + const task = await db.createTask(taskData); + await sendTaskCreatedMessage(toJid, accountId, task); + } +} + +// READ +case 'interactive_message_actions': { + if (actionItem.value.startsWith('view_task')) { + const taskId = actionItem.value.split('_')[2]; + const task = await db.getTask(taskId); + await sendTaskDetails(toJid, accountId, task); + } +} + +// UPDATE +case 'interactive_message_actions': { + if (actionItem.value.startsWith('complete_task')) { + const taskId = actionItem.value.split('_')[2]; + await db.updateTaskStatus(taskId, 'completed'); + await sendStatusUpdate(toJid, accountId, taskId); + } +} + +// DELETE +case 'interactive_message_actions': { + if (actionItem.value.startsWith('delete_task')) { + const taskId = actionItem.value.split('_')[2]; + await db.deleteTask(taskId); + await sendDeletionConfirmation(toJid, accountId, taskId); + } +} +``` + +**Recommended For**: Full application architecture + +## Common Patterns Across Samples + +### 1. Environment Variable Management + +All samples use `.env` files with similar structure: + +```bash +# Authentication +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# Third-party APIs (if applicable) +ANTHROPIC_API_KEY= +UNSPLASH_ACCESS_KEY= + +# Server +PORT=4000 +NODE_ENV=development +``` + +### 2. Project Structure + +Common folder organization: + +``` +sample-app/ +├── routes/ +│ ├── webhook.js # Webhook handlers +│ └── oauth.js # OAuth flows (if needed) +├── utils/ +│ ├── zoom-api.js # Zoom API wrappers +│ ├── auth.js # Token management +│ └── validation.js # Input validation +├── models/ # Database models (if applicable) +├── views/ # Frontend templates (if applicable) +├── server.js # Express app +├── .env.example +└── package.json +``` + +### 3. Webhook Verification + +All samples verify webhook signatures: + +```javascript +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const hash = crypto.createHmac('sha256', SECRET_TOKEN) + .update(message) + .digest('hex'); + + return signature === `v0=${hash}`; +} +``` + +### 4. Error Handling + +Consistent error handling pattern: + +```javascript +app.post('/webhook', async (req, res) => { + try { + verifyWebhook(req); + await handleWebhook(req.body); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Webhook error:', error); + + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + res.status(500).json({ error: 'Internal server error' }); + } +}); +``` + +### 5. Async Webhook Processing + +Respond immediately, process async: + +```javascript +app.post('/webhook', (req, res) => { + // Respond immediately + res.status(200).json({ success: true }); + + // Process asynchronously + processWebhookAsync(req.body).catch(error => { + console.error('Async processing error:', error); + }); +}); +``` + +## Architecture Lessons + +### Chatbot Lifecycle + +Common lifecycle across all samples: + +``` +1. User Action (slash command, button click, message) + ↓ +2. Zoom sends webhook to Bot Endpoint URL + ↓ +3. Server verifies signature + ↓ +4. Server responds 200 (immediately) + ↓ +5. Server processes request (async) + ↓ +6. Server calls external APIs if needed + ↓ +7. Server sends chatbot message back to Zoom +``` + +### State Management + +**Simple bots**: In-memory state (Map/Object) +**Production bots**: Database (PostgreSQL, MongoDB, Redis) + +```javascript +// Simple (development) +const userState = new Map(); + +// Production +const userState = { + async get(userId) { + return await db.query('SELECT * FROM user_state WHERE user_id = $1', [userId]); + }, + async set(userId, state) { + return await db.query('INSERT INTO user_state (user_id, state) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET state = $2', [userId, state]); + } +}; +``` + +## Deprecation Notes + +Some samples may use deprecated patterns: + +### ❌ Old Pattern (Don't Use) +```javascript +// Hardcoded credentials +const CLIENT_ID = 'abc123'; +``` + +### ✅ New Pattern (Use This) +```javascript +// Environment variables +const CLIENT_ID = process.env.ZOOM_CLIENT_ID; +``` + +### ❌ Old Pattern (Don't Use) +```javascript +// Synchronous webhook processing (may timeout) +app.post('/webhook', async (req, res) => { + await longRunningProcess(); + res.status(200).json({ success: true }); +}); +``` + +### ✅ New Pattern (Use This) +```javascript +// Async processing +app.post('/webhook', (req, res) => { + res.status(200).json({ success: true }); + longRunningProcess().catch(console.error); +}); +``` + +## Sample Selection Guide + +### Choose chatbot-nodejs-quickstart if: +- You're new to Zoom chatbots +- You want a tutorial series +- You need step-by-step guidance + +### Choose zoom-chatbot-claude-sample if: +- You want to integrate an LLM +- You need conversational AI +- You want to see LLM integration patterns + +### Choose unsplash-chatbot if: +- You need to integrate external APIs +- You want database patterns +- You need user preference storage + +### Choose zoom-erp-chatbot-sample if: +- You're building enterprise integrations +- You need scheduled notifications +- You want approval workflows + +### Choose task-manager-sample if: +- You want a full CRUD application +- You need complex state management +- You want to see production architecture + +## Next Steps + +- [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your own using these patterns +- [LLM Integration Example](../examples/llm-integration.md) - Integrate Claude/GPT +- [Button Actions Example](../examples/button-actions.md) - Handle interactive components +- [Sample Comparison](sample-comparison.md) - Compare common sample shapes before choosing a baseline + +## Resources + +- [Official Samples GitHub Org](https://github.com/zoom?q=chatbot) +- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/) +- [Developer Forum](https://devforum.zoom.us/) diff --git a/partner-built/zoom-plugin/skills/team-chat/references/scopes.md b/partner-built/zoom-plugin/skills/team-chat/references/scopes.md new file mode 100644 index 00000000..c970bb3c --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/scopes.md @@ -0,0 +1,17 @@ +# Scopes Reference (Common) + +## Team Chat API + +Common scopes include: + +- `chat_message:write` +- `chat_channel:read` + +## Chatbot API + +The Chatbot feature uses bot credentials; typical setup includes enabling the feature and using the bot token. + +## Pitfall + +After adding scopes in Marketplace, users often need to reauthorize to grant them. + diff --git a/partner-built/zoom-plugin/skills/team-chat/references/webhook-events.md b/partner-built/zoom-plugin/skills/team-chat/references/webhook-events.md new file mode 100644 index 00000000..483ae8ca --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/references/webhook-events.md @@ -0,0 +1,17 @@ +# Webhook Events (Chatbot API) + +Common webhook event types you will handle: + +- `bot_notification`: user messages your bot or triggers a command +- `interactive_message_actions`: user clicks a button +- `chat_message.submit`: user submits a form +- `bot_installed`: bot added to an account +- `app_deauthorized`: bot removed / app deauthorized + +## Handler Checklist + +- Verify the request (per Zoom's verification guidance). +- Parse payload carefully (treat as untrusted input). +- Route by event type and action values. +- Respond quickly; do heavy work async if needed. + diff --git a/partner-built/zoom-plugin/skills/team-chat/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/common-issues.md new file mode 100644 index 00000000..37742664 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/common-issues.md @@ -0,0 +1,428 @@ +# Common Issues and Solutions + +Quick diagnostics and solutions for Zoom Team Chat development. + +## Authentication Issues + +### "Invalid client_id or client_secret" + +**Cause**: Incorrect credentials or using wrong environment (dev vs production) + +**Solution**: +1. Verify credentials in `.env` match Zoom Marketplace +2. Check you're using Development credentials (not Production) +3. Regenerate Client Secret if needed + +### "Get Bot Token" returns 404 or HTML page + +**Cause**: Using wrong token endpoint. + +**Fix**: +- Use `https://zoom.us/oauth/token` for token exchange. +- Do not use `https://zoom.us/oauth/token` for chatbot token requests. + +Quick check: +```bash +curl -X POST https://zoom.us/oauth/token \ + -H "Authorization: Basic " \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" +``` + +### "Token expired" + +**Cause**: Access token has expired (1 hour for user tokens) + +**Solution**: +```javascript +// Implement token refresh +if (error.message.includes('token expired')) { + const newToken = await refreshAccessToken(refreshToken); + // Retry request with new token +} +``` + +### "Scope not authorized" + +**Cause**: Missing required scope in app configuration + +**Solution**: +1. Go to Zoom Marketplace → Your App → Scopes +2. Add missing scope (e.g., `chat_message:write`) +3. Users must re-authorize the app + +## Webhook Issues + +### "Cannot GET /webhook" (Browser) + +**Expected Behavior**: This is NORMAL + +**Explanation**: Webhooks are POST-only. Browsers send GET requests. + +**Test properly**: +```bash +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000" + +# Use POST instead +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -d '{"event":"test"}' +``` + +### "Invalid webhook signature" + +**Cause**: Mismatch between your Secret Token and Zoom's + +**Solution**: +1. Verify `ZOOM_VERIFICATION_TOKEN` in `.env` +2. Check Secret Token in Zoom Marketplace → Features → Team Chat Subscriptions +3. Ensure no extra spaces/characters in token + +**Debug**: +```javascript +console.log('Expected token:', process.env.ZOOM_VERIFICATION_TOKEN); +console.log('Signature from Zoom:', req.headers['x-zm-signature']); +``` + +### URL Validation Fails + +**Cause**: Incorrect response format + +**Correct response**: +```javascript +{ + "plainToken": "xyz123", + "encryptedToken": "hmac_sha256_hash" +} +``` + +**Incorrect**: +```javascript +{ "success": true } // Wrong! +``` + +### No Webhooks Received + +**Checklist**: +- [ ] ngrok is running: `ngrok http 4000` +- [ ] Bot Endpoint URL in Zoom Marketplace matches ngrok URL +- [ ] Server is running: `node server.js` +- [ ] Slash command configured in Zoom Marketplace +- [ ] Bot installed in your account + +**Test**: +```bash +# In Zoom Team Chat, type: +/yourbot test + +# Should see webhook in server logs +``` + +## Bot JID Issues + +### "Bot JID not found" + +**Cause**: Chatbot feature not enabled + +**Solution**: +1. Go to Zoom Marketplace → Your App → Features +2. Toggle **Chatbot** ON +3. Bot JID will appear in **Bot Credentials** section + +### "Bot JID appears but messages not sending" + +**Cause**: Wrong Bot JID format or environment mismatch + +**Solution**: +1. Verify format: `v1abc123xyz@xmpp.zoom.us` +2. Use Development Bot JID for testing +3. Check Account ID matches the bot's account + +## Message Sending Issues + +### "Messages not appearing in Team Chat" + +**Common causes**: + +1. **Wrong `to_jid`** + ```javascript + // Use toJid from webhook payload + await sendMessage(payload.toJid, accountId, content); + ``` + +2. **Missing `account_id`** + ```javascript + // Required for chatbot messages + { + "account_id": process.env.ZOOM_ACCOUNT_ID, // Don't forget! + "robot_jid": process.env.ZOOM_BOT_JID, + "to_jid": toJid + } + ``` + +3. **Incorrect content format** + ```javascript + // ❌ Wrong + { "text": "Hello" } + + // ✅ Correct + { + "content": { + "body": [ + { "type": "message", "text": "Hello" } + ] + } + } + ``` + +### "Message truncated or garbled" + +**Cause**: Special characters or exceeding 4096 char limit + +**Solution**: +```javascript +function sanitizeMessage(message) { + return message + .trim() + .replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars + .substring(0, 4096); // Enforce limit +} +``` + +## Button/Form Issues + +### "Buttons not clickable" + +**Cause**: Missing `value` field + +**Incorrect**: +```javascript +{ + "type": "actions", + "items": [ + { "text": "Click Me" } // Missing value! + ] +} +``` + +**Correct**: +```javascript +{ + "type": "actions", + "items": [ + { "text": "Click Me", "value": "clicked" } + ] +} +``` + +### "Button clicks not triggering webhooks" + +**Checklist**: +- [ ] Webhook handler has `interactive_message_actions` case +- [ ] Bot Endpoint URL configured correctly +- [ ] Server responding with 200 status +- [ ] Webhook signature verification passing + +## ngrok Issues + +### "ngrok session expired" + +**Cause**: Free ngrok URLs expire after 2 hours + +**Solutions**: +1. **Short-term**: Restart ngrok, update Bot Endpoint URL +2. **Long-term**: Use ngrok paid plan or deploy to production + +### "ngrok URL changes every restart" + +**Free plan behavior**: URL changes each time + +**Solutions**: +1. Use ngrok auth token for persistent URLs (paid) +2. Use environment variable for flexibility: + ```javascript + const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://YOUR_PUBLIC_WEBHOOK_URL/webhook'; + ``` + +## Deployment Issues + +### "Works locally but not in production" + +**Common causes**: + +1. **Environment variables not set** + ```bash + # Verify all vars exist + echo $ZOOM_CLIENT_ID + echo $ZOOM_CLIENT_SECRET + echo $ZOOM_BOT_JID + ``` + +2. **HTTP instead of HTTPS** + - Production MUST use HTTPS + - Zoom rejects HTTP endpoints + +3. **Port binding issues** + ```javascript + // Use PORT from environment + const PORT = process.env.PORT || 4000; + ``` + +4. **Credentials exist, but wrong `.env` file is loaded** + - If your app keeps per-mode env files (for example `project/team-chat-api/.env` and `project/chatbot-api/.env`), make sure runtime loads those files explicitly. + - Verify loaded config via a health/config endpoint before debugging OAuth logic. + +### `404` on `/team-chat/api/channel/*` + +**Cause**: Route mismatch between old and new demo structure. + +**Fix**: +- New pages should use: + - `/team-chat/user-demo` + - `/team-chat/bot-demo` +- Keep compatibility routes in backend if older UI still calls: + - `/api/channel/list` + - `/api/channel/messages` + - `/api/channel/message` + +### Browser shows `ERR_BLOCKED_BY_CLIENT` + +**Cause**: Browser extension/adblock/privacy filter blocked a request. + +**What to do**: +- Test in Incognito or with extensions disabled for your host. +- Confirm backend route with `curl` before treating this as server failure. + +## Rate Limiting + +### "Rate limit exceeded" + +**Zoom Limits**: +- 10 requests/second per user +- 100 requests/second per app + +**Solution**: +```javascript +// Implement exponential backoff +async function retryWithBackoff(fn, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (error.status === 429) { + const delay = Math.pow(2, i) * 1000; + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + throw new Error('Max retries exceeded'); +} +``` + +## General App Issues + +### "App not appearing in Team Chat" + +**Cause**: Team Chat surface not enabled + +**Solution**: +1. Go to Zoom Marketplace → Your App → Features → Surface +2. Check **Team Chat** +3. Configure Home URL and Domain Allow List +4. Save changes + +### "Users can't install the app" + +**Cause**: App not in Local Test or not published + +**Solutions**: +1. **For testing**: Go to Local Test → Generate Authorization URL → Share with team +2. **For production**: Submit app for Zoom review and publish + +## Debugging Tools + +### Log All Webhooks + +```javascript +app.post('/webhook', (req, res) => { + console.log('=== Webhook Received ==='); + console.log('Event:', req.body.event); + console.log('Payload:', JSON.stringify(req.body.payload, null, 2)); + console.log('Headers:', req.headers); + // ... handle webhook +}); +``` + +### Test Token Generation + +```javascript +// Test script: test-token.js +require('dotenv').config(); +const { getChatbotToken } = require('./utils/auth'); + +(async () => { + try { + const token = await getChatbotToken(); + console.log('✅ Token generated successfully'); + console.log('Token:', token.substring(0, 20) + '...'); + } catch (error) { + console.error('❌ Token error:', error.message); + } +})(); +``` + +### Verify Credentials + +```javascript +// verify-setup.js +require('dotenv').config(); + +const required = [ + 'ZOOM_CLIENT_ID', + 'ZOOM_CLIENT_SECRET', + 'ZOOM_BOT_JID', + 'ZOOM_VERIFICATION_TOKEN', + 'ZOOM_ACCOUNT_ID' +]; + +console.log('=== Credential Check ==='); +required.forEach(key => { + const value = process.env[key]; + if (!value) { + console.error(`❌ Missing: ${key}`); + } else { + console.log(`✅ ${key}: ${value.substring(0, 10)}...`); + } +}); +``` + +## Getting Help + +### Before Asking for Help + +1. Check error messages in console/logs +2. Verify all credentials are correct +3. Test with curl or Postman +4. Review [official samples](https://github.com/zoom?q=chatbot) + +### Where to Get Help + +- [Zoom Developer Forum](https://devforum.zoom.us/) +- [GitHub Issues](https://github.com/zoom/chatbot-nodejs-quickstart/issues) +- [Developer Support](https://devsupport.zoom.us) + +### Include in Support Requests + +1. Zoom app type (General App OAuth) +2. Error message (full text) +3. Code snippet (sanitized - no credentials!) +4. Steps to reproduce +5. Expected vs actual behavior + +## Next Steps + +- [Webhook Architecture](../concepts/webhooks.md) - Deep dive into webhooks +- [Chatbot Setup](../examples/chatbot-setup.md) - Complete working example +- [API Reference](../references/api-reference.md) - Endpoint documentation diff --git a/partner-built/zoom-plugin/skills/team-chat/troubleshooting/deployment-issues.md b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/deployment-issues.md new file mode 100644 index 00000000..c0cfa2aa --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/deployment-issues.md @@ -0,0 +1,19 @@ +# Deployment Issues + +## Works Locally, Fails in Prod + +- DNS/HTTPS misconfiguration +- blocked outbound calls from your environment +- missing env vars / secrets +- wrong env file loaded at runtime (for split setups like `team-chat-api/.env` and `chatbot-api/.env`) + +## Quick Prod Checklist + +- Confirm token endpoint is `https://zoom.us/oauth/token` +- Confirm user OAuth authorize URL is `https://zoom.us/oauth/authorize` +- Confirm current UI routes are `/team-chat/user-demo` and `/team-chat/bot-demo` +- Confirm reverse proxy forwards `/team-chat/api/*` correctly + +## Webhooks Time Out + +- Respond fast and move long-running work to async jobs. diff --git a/partner-built/zoom-plugin/skills/team-chat/troubleshooting/message-issues.md b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/message-issues.md new file mode 100644 index 00000000..80d8d446 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/message-issues.md @@ -0,0 +1,13 @@ +# Message Issues + +## Messages Not Sending + +- Confirm you're using the correct API: + - Team Chat API uses user OAuth token + - Chatbot API uses bot token + `robot_jid` + +## Card Not Rendering + +- Validate the card JSON payload against known-good examples. +- Simplify to a minimal card and add components incrementally. + diff --git a/partner-built/zoom-plugin/skills/team-chat/troubleshooting/oauth-issues.md b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/oauth-issues.md new file mode 100644 index 00000000..3bad6715 --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/oauth-issues.md @@ -0,0 +1,25 @@ +# OAuth Issues (Team Chat API) + +## "Invalid redirect" / redirect mismatch + +- The redirect URL in the token exchange must exactly match what's configured in Marketplace. +- Keep endpoint split correct: + - authorize: `https://zoom.us/oauth/authorize` + - token exchange: `https://zoom.us/oauth/token` + +## "Invalid access token, does not contain scopes" + +- Add the scope in Marketplace. +- Ensure the user re-authorizes after scope changes. +- Confirm you're using the user token for Team Chat API calls. + +## Token Expired + +- Refresh access tokens using the refresh token. +- If refresh fails, the user likely needs to reauthorize. + +## Callback succeeds but app still has no token + +- Verify callback route actually exchanges `code` server-side. +- Verify `state` is validated and not expired. +- Verify token is persisted where your UI expects it (session/database/local storage for demo). diff --git a/partner-built/zoom-plugin/skills/team-chat/troubleshooting/webhook-issues.md b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/webhook-issues.md new file mode 100644 index 00000000..813f89eb --- /dev/null +++ b/partner-built/zoom-plugin/skills/team-chat/troubleshooting/webhook-issues.md @@ -0,0 +1,13 @@ +# Webhook Issues (Chatbot API) + +## No Events Arriving + +- Ensure your endpoint is publicly reachable over HTTPS. +- Confirm the correct app/account is installed and subscribed. +- Check verification settings (secret token, validation flow). + +## Duplicate Events + +- Webhooks can be delivered more than once. +- Add idempotency (store processed event IDs if available). + diff --git a/partner-built/zoom-plugin/skills/ui-toolkit/RUNBOOK.md b/partner-built/zoom-plugin/skills/ui-toolkit/RUNBOOK.md new file mode 100644 index 00000000..3711062f --- /dev/null +++ b/partner-built/zoom-plugin/skills/ui-toolkit/RUNBOOK.md @@ -0,0 +1,63 @@ +# UI Toolkit 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common UI Toolkit failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Token Source + +- UI Toolkit still needs Video SDK JWT. +- Generate JWT server-side only (never expose SDK secret client-side). + +## 2) Confirm Basic Config + +- `videoSDKJWT`, `sessionName`, `userName` must be present. +- Verify enabled features match your expected UI behavior. + +## 3) Confirm Framework Constraints + +- Validate your installed package peer dependencies (React version mismatch is common). +- In SSR frameworks, load toolkit client-side and clean up on unmount. + +## 4) Confirm CSS and Lifecycle + +- Ensure toolkit CSS is loaded. +- Call `closeSession`/`destroy` on teardown to avoid stale UI state. + +## 5) Confirm Deployment Paths + +- In basePath/subpath deployments, verify API route URLs and asset paths. +- If API returns HTML instead of JSON, re-check route mapping/proxy. + +## 6) Quick Probes + +- Token endpoint returns JSON token. +- `joinSession` succeeds and session events fire. +- Closing session cleans up container without errors. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify token endpoint responds with JSON +curl -sS -i "$UI_TOOLKIT_BASE_URL/api/token" + +# 2) Verify app route is reachable +curl -sS -i "$UI_TOOLKIT_BASE_URL" +``` + +Expected: valid JSON for token endpoint and valid HTML for app route. + +## 7) Fast Decision Tree + +- **Session won't join** -> invalid/missing JWT or bad session config. +- **UI partially broken** -> missing CSS or unsupported feature config. +- **Works local, fails prod** -> basePath/proxy mismatch. + +## 8) SDK Selection Guardrail + +- Use UI Toolkit for low-code prebuilt Video SDK UI. +- Use raw Video SDK for full custom rendering and control. diff --git a/partner-built/zoom-plugin/skills/ui-toolkit/SKILL.md b/partner-built/zoom-plugin/skills/ui-toolkit/SKILL.md new file mode 100644 index 00000000..c0328419 --- /dev/null +++ b/partner-built/zoom-plugin/skills/ui-toolkit/SKILL.md @@ -0,0 +1,555 @@ +--- +name: ui-toolkit/web +description: "Reference skill for Zoom Video SDK UI Toolkit. Use after routing to a web video workflow when you want prebuilt React UI instead of building a fully custom Video SDK interface." +user-invocable: false +triggers: + - "ui toolkit" + - "zoom ui" + - "prebuilt video ui" + - "video conferencing ui" + - "zoom video ui toolkit" + - "uitoolkit" + - "ready-made zoom ui" +--- + +# Zoom Video SDK UI Toolkit + +Background reference for the prebuilt Zoom Video SDK UI Toolkit on web. Prefer `choose-zoom-approach` first when the user might still need Meeting SDK instead. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +**API Reference**: https://marketplacefront.zoom.us/sdk/uitoolkit/web/ +**NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +**Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit + +## Quick Links + +**New to UI Toolkit? Follow this path:** + +1. **Quick Start** - Get running in 5 minutes (see below) +2. **JWT Authentication** - Server-side token generation (required) +3. **Composite vs Components** - Choose your approach +4. **Framework Integration** - React, Vue, Angular, Next.js patterns +5. **Integrated Index** - see the section below in this file + +**Having issues?** +- Session not joining → Check JWT Authentication (most common issue) +- React 18 peer dependency error → See Installation section +- CSS not loading → See [Troubleshooting](troubleshooting/common-issues.md) +- Components not showing → Check Component Lifecycle +- Start with preflight checks → [5-Minute Runbook](RUNBOOK.md) + +## Overview + +The Zoom Video SDK UI Toolkit is a **pre-built video UI library** that renders complete video conferencing experiences with minimal code. Unlike the raw Video SDK, the UI Toolkit provides: + +- ✅ **Ready-to-use UI** - Professional video interface out of the box +- ✅ **Zero UI code** - No need to build video layouts, controls, or participant management +- ✅ **Framework agnostic** - Works with React, Vue, Angular, Next.js, vanilla JS +- ✅ **Highly customizable** - Choose which features to enable, customize themes +- ✅ **Built-in features** - Chat, screen share, settings, virtual backgrounds included + +**When to use UI Toolkit:** +- You want a complete video solution quickly +- You need Zoom-like UI consistency +- You don't want to build custom video UI +- You need standard features (chat, share, participants) + +**When to use raw Video SDK instead:** +- You need complete custom UI control +- You're building a non-standard video experience +- You need access to raw video/audio data +- You want to build your own rendering pipeline + +## Installation + +```bash +npm install @zoom/videosdk-zoom-ui-toolkit jsrsasign +npm install -D @types/jsrsasign +``` + +**Note**: React support depends on the UI Toolkit version. Check the package peer dependencies for your installed version (React 18 is commonly required). + +## Quick Start + +### Basic Usage (Vanilla JS) + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const container = document.getElementById("sessionContainer"); + +const config = { + videoSDKJWT: "your_jwt_token", + sessionName: "my-session", + userName: "John Doe", + sessionPasscode: "", + features: ["video", "audio", "share", "chat", "users", "settings"], +}; + +uitoolkit.joinSession(container, config); + +uitoolkit.onSessionJoined(() => { + console.log("Session joined"); +}); + +uitoolkit.onSessionClosed(() => { + console.log("Session closed"); +}); +``` + +### Next.js / React Integration + +```typescript +'use client'; + +import { useEffect, useRef } from 'react'; + +export default function VideoSession({ jwt, sessionName, userName }) { + const containerRef = useRef(null); + const uitoolkitRef = useRef(null); + + useEffect(() => { + let isMounted = true; + + const init = async () => { + const uitoolkitModule = await import('@zoom/videosdk-zoom-ui-toolkit'); + const uitoolkit = uitoolkitModule.default; + uitoolkitRef.current = uitoolkit; + + // If TypeScript complains about CSS imports, configure your app to allow them + // (for example via a global `declare module \"*.css\";`), or import the CSS from + // a global entrypoint (Next.js layout/_app) instead of inlining here. + await import('@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css'); + + if (!isMounted || !containerRef.current) return; + + const config: any = { + videoSDKJWT: jwt, + sessionName: sessionName, + userName: userName, + sessionPasscode: '', + features: ['video', 'audio', 'share', 'chat', 'users', 'settings'], + }; + + uitoolkit.joinSession(containerRef.current, config); + uitoolkit.onSessionJoined(() => console.log('Joined')); + uitoolkit.onSessionClosed(() => console.log('Closed')); + }; + + init(); + + return () => { + isMounted = false; + if (uitoolkitRef.current && containerRef.current) { + try { + uitoolkitRef.current.closeSession(containerRef.current); + } catch (e) {} + } + }; + }, [jwt, sessionName, userName]); + + return
; +} +``` + +## Available Features + +| Feature | Description | +|---------|-------------| +| `video` | Enable video layout and send/receive video | +| `audio` | Show audio button, send/receive audio | +| `share` | Screen sharing | +| `chat` | In-session messaging | +| `users` | Participant list | +| `settings` | Device selection, virtual background | +| `preview` | Pre-join camera/mic preview | +| `recording` | Cloud recording (paid plan) | +| `leave` | Leave/end session button | + +## Troubleshooting + +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - CSS, SSR, JWT/session join, customization limits + +## JWT Token Generation (Server-Side) + +**Required**: Generate JWT tokens on your server, never expose SDK secret client-side. + +### Node.js / Next.js API Route + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { KJUR } from 'jsrsasign'; + +const ZOOM_VIDEO_SDK_KEY = process.env.ZOOM_VIDEO_SDK_KEY; +const ZOOM_VIDEO_SDK_SECRET = process.env.ZOOM_VIDEO_SDK_SECRET; + +export async function POST(request: NextRequest) { + const { sessionName, role, userName } = await request.json(); + + if (!sessionName || role === undefined) { + return NextResponse.json({ error: 'Missing params' }, { status: 400 }); + } + + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 60 * 60 * 2; // 2 hours + + const oHeader = { alg: 'HS256', typ: 'JWT' }; + const oPayload = { + app_key: ZOOM_VIDEO_SDK_KEY, + role_type: role, // 0 = participant, 1 = host + tpc: sessionName, + version: 1, + iat, + exp, + user_identity: userName || 'User', + }; + + const signature = KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify(oHeader), + JSON.stringify(oPayload), + ZOOM_VIDEO_SDK_SECRET + ); + + return NextResponse.json({ signature }); +} +``` + +### JWT Payload Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `app_key` | Yes | Your Video SDK Key | +| `role_type` | Yes | 0 = participant, 1 = host | +| `tpc` | Yes | Session/topic name | +| `version` | Yes | Always 1 | +| `iat` | Yes | Issued at (Unix timestamp) | +| `exp` | Yes | Expiration (Unix timestamp) | +| `user_identity` | No | User identifier | + +## API Reference + +### Core Methods + +```javascript +uitoolkit.joinSession(container, config); +uitoolkit.closeSession(container); +``` + +### Event Listeners + +```javascript +uitoolkit.onSessionJoined(callback); +uitoolkit.onSessionClosed(callback); +uitoolkit.offSessionJoined(callback); +uitoolkit.offSessionClosed(callback); +``` + +### Component Methods + +```javascript +uitoolkit.showChatComponent(container); +uitoolkit.hideChatComponent(container); +uitoolkit.showUsersComponent(container); +uitoolkit.hideUsersComponent(container); +uitoolkit.showControlsComponent(container); +uitoolkit.hideControlsComponent(container); +uitoolkit.showSettingsComponent(container); +uitoolkit.hideSettingsComponent(container); +uitoolkit.hideAllComponents(); +``` + +## CDN Usage (No Build Step) + +```html + + + +
+ + +``` + +## Next.js with basePath + +When deploying Next.js under a subpath: + +```typescript +// next.config.ts +const nextConfig = { + basePath: "/your-app-path", + assetPrefix: "/your-app-path", +}; +``` + +Fetch API routes with full path: +```typescript +fetch('/your-app-path/api/token', { ... }) +``` + +## Prerequisites + +1. **Zoom Video SDK credentials** from [Zoom Marketplace](https://marketplace.zoom.us/) +2. **React** version compatible with your installed UI Toolkit package (check peer deps; React 18 is common) +3. **Server-side JWT generation** (never expose SDK secret) +4. **Modern browser** with WebRTC support + +## Browser Support + +| Browser | Version | +|---------|---------| +| Chrome | 78+ | +| Firefox | 76+ | +| Safari | 14.1+ | +| Edge | 79+ | + +## Common Issues + +| Issue | Solution | +|-------|----------| +| `peer react@"^18.0.0"` error | Use the React version required by the installed UI Toolkit package (check peer deps; React 18 is common) | +| CSS import TypeScript error | Configure TS/CSS handling (prefer a global `*.css` module declaration); avoid `@ts-ignore` except in throwaway demos | +| Config type error | Type config as `any` | +| API returns HTML not JSON | Check basePath in fetch URL | + +## Resources + +- **GitHub**: https://github.com/zoom/videosdk-zoom-ui-toolkit-web +- **UI Toolkit Docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **Auth Endpoint Sample**: https://github.com/zoom/videosdk-auth-endpoint-sample +- **Marketplace**: https://marketplace.zoom.us/ + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +Complete navigation for all UI Toolkit documentation. + +## 📚 Start Here + +New to the UI Toolkit? Follow this learning path: + +1. **[SKILL.md](SKILL.md)** - Main overview and quick start +2. **[5-Minute Runbook](RUNBOOK.md)** - Preflight checks before deep debugging +3. **Quick Start Guide** - Working code in 5 minutes (see skill.md) +4. **JWT Authentication** - Server-side token generation (see skill.md) +5. **Choose Your Mode** - Composite vs Components (see skill.md) + +## 🎯 Core Concepts + +Understanding how UI Toolkit works: + +- **Composite vs Components** - Two ways to use UI Toolkit (see skill.md) +- **UI Toolkit Architecture** - How it wraps Video SDK internally +- **Feature Configuration** - Understanding featuresOptions structure +- **Session Lifecycle** - Join → Active → Leave/Close → Destroy flow + +## 📖 Complete Guides + +### Getting Started +- **Installation** - NPM install and React 18 setup (see skill.md) +- **Quick Start - Composite** - Full UI in one container (see skill.md) +- **Quick Start - Components** - Individual UI pieces (see skill.md) +- **JWT Authentication** - Server-side token generation (see skill.md) + +### Framework Integration +- **React Integration** - Hooks, useEffect patterns (see skill.md) +- **Vue.js Integration** - Composition API and Options API (see skill.md) +- **Angular Integration** - Component lifecycle (see skill.md) +- **Next.js Integration** - App Router, Server Components (see skill.md) +- **Vanilla JavaScript** - No framework usage (see skill.md) + +### Advanced Topics +- **Component Lifecycle** - Mount, unmount, cleanup patterns +- **Event Listeners** - React to session events +- **Session Management** - Programmatic control +- **Quality Statistics** - Monitor connection quality +- **Custom Themes** - Theme customization +- **Virtual Backgrounds** - Custom background images + +## 📚 API Reference + +Complete API documentation: + +- **Core Methods** (see skill.md) + - `joinSession()` - Start a video session + - `closeSession()` - End session and remove UI + - `destroy()` - Clean up UI Toolkit instance + - `leaveSession()` - Leave without destroying UI + +- **Component Methods** (see skill.md) + - `showControlsComponent()` - Display control bar + - `showChatComponent()` - Display chat panel + - `showUsersComponent()` - Display participants list + - `showSettingsComponent()` - Display settings panel + - `hideAllComponents()` - Hide all components + +- **Event Listeners** (see skill.md) + - `onSessionJoined()` - Session joined successfully + - `onSessionClosed()` - Session ended + - `onSessionDestroyed()` - UI Toolkit destroyed + - `onViewTypeChange()` - View mode changed + - `on()` - Subscribe to Video SDK events + - `off()` - Unsubscribe from events + +- **Information Methods** (see skill.md) + - `getSessionInfo()` - Get session details + - `getCurrentUserInfo()` - Get current user + - `getAllUser()` - Get all participants + - `getClient()` - Get underlying Video SDK client + - `version()` - Get version info + +- **Control Methods** (see skill.md) + - `changeViewType()` - Switch view mode + - `mirrorVideo()` - Mirror self video + - `isSupportCustomLayout()` - Check device support + +- **Statistics Methods** (see skill.md) + - `subscribeAudioStatisticData()` - Audio quality stats + - `subscribeVideoStatisticData()` - Video quality stats + - `subscribeShareStatisticData()` - Share quality stats + +## 🔧 Configuration + +- **Feature Configuration** (see skill.md) + - `featuresOptions` structure + - Audio/Video options + - Chat, Users, Settings + - Virtual Background + - Recording, Captions (paid features) + - Theme customization + - View modes + +- **Session Configuration** (see skill.md) + - Required: `videoSDKJWT`, `sessionName`, `userName` + - Optional: `sessionPasscode`, `sessionIdleTimeoutMins` + - Debug mode + - Web endpoint + - Language settings + +## ⚠️ Troubleshooting + +### Common Issues +- React 18 peer dependency error +- JWT token invalid +- CSS not loading +- Components not showing +- Session join failures + +See: **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** + +### Framework-Specific Issues +- React: SSR, hydration, cleanup +- Vue: Reactivity, lifecycle +- Angular: Module imports, AOT +- Next.js: App Router, basePath + +### Session Issues +- Authentication failures +- Connection problems +- Video/audio not working +- Screen share issues + +## 📦 Sample Applications + +**Official Repositories**: + +| Framework | Repository | Key Features | +|-----------|------------|--------------| +| React | [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | Hooks, TypeScript | +| Vue.js | [videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | Composition API | +| Angular | [videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | Services, Guards | +| JavaScript | [videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | Vanilla JS | +| Auth Endpoint | [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | Node.js JWT | + +## 🌐 External Resources + +- **Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/uitoolkit/web/ +- **NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **Marketplace**: https://marketplace.zoom.us/ +- **Developer Forum**: https://devforum.zoom.us/ +- **Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit +- **Changelog**: https://developers.zoom.us/changelog/ui-toolkit/web/ + +## 🎓 Learning Path + +### Beginner +1. Read [SKILL.md](SKILL.md) overview +2. Follow Quick Start - Composite +3. Generate JWT on server +4. Join your first session +5. Explore available features + +### Intermediate +1. Try Component Mode +2. Add event listeners +3. Customize theme +4. Add virtual backgrounds +5. Integrate with your framework + +### Advanced +1. Access underlying Video SDK +2. Subscribe to quality statistics +3. Handle all edge cases +4. Implement custom layouts +5. Build production-ready app + +## 📋 Quick Reference Card + +### Minimal Working Example + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const config = { + videoSDKJWT: "YOUR_JWT", + sessionName: "test-session", + userName: "User", + featuresOptions: { + video: { enable: true }, + audio: { enable: true } + } +}; + +uitoolkit.joinSession(document.getElementById("container"), config); +uitoolkit.onSessionJoined(() => console.log("Joined")); +uitoolkit.onSessionClosed(() => uitoolkit.destroy()); +``` + +### Must-Remember Rules + +1. ✅ **Always** generate JWT server-side +2. ✅ **Always** call `destroy()` on cleanup +3. ✅ **Always** use React 18 (not 17/19) +4. ✅ **Always** import CSS file +5. ❌ **Never** expose SDK secret client-side +6. ❌ **Never** skip `onSessionClosed` cleanup +7. ❌ **Never** call components before `joinSession` + +## 📞 Support + +- **Developer Forum**: https://devforum.zoom.us/ +- **Developer Support**: https://developers.zoom.us/support/ +- **Premier Support**: https://explore.zoom.us/en/support-plans/developer/ + +--- + +**Navigation**: [← Back to SKILL.md](SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/ui-toolkit/references/environment-variables.md b/partner-built/zoom-plugin/skills/ui-toolkit/references/environment-variables.md new file mode 100644 index 00000000..fe5c5bef --- /dev/null +++ b/partner-built/zoom-plugin/skills/ui-toolkit/references/environment-variables.md @@ -0,0 +1,19 @@ +# Zoom UI Toolkit (Video SDK) Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_VIDEO_SDK_KEY` | Yes | Video SDK identity for token generation | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Yes | Server-side token/signature generation | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_UI_TOOLKIT_BASE_URL` | Optional | App base URL used by your token service | Set to your deployed app origin | + +## Runtime-only values + +- `VIDEO_SDK_JWT` + +Generate server-side and return short-lived tokens to the browser. + +## Notes + +- UI Toolkit depends on Video SDK auth; keep `ZOOM_VIDEO_SDK_SECRET` off the client. diff --git a/partner-built/zoom-plugin/skills/ui-toolkit/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/ui-toolkit/troubleshooting/common-issues.md new file mode 100644 index 00000000..02f6a817 --- /dev/null +++ b/partner-built/zoom-plugin/skills/ui-toolkit/troubleshooting/common-issues.md @@ -0,0 +1,41 @@ +# Common Issues + +Quick diagnostics for Zoom Video SDK UI Toolkit (Web). + +## CSS Not Loading / UI Looks Unstyled + +**Fix**: +- Ensure you import the toolkit CSS: + - `@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css` +- For bundlers, verify CSS handling is enabled (Vite/Next.js). + +## SSR / Next.js Errors ("window is not defined") + +**Fix**: +- Load UI Toolkit only on the client (dynamic import in a client component). + +## Session Doesn't Join + +**Common causes**: +- Missing/invalid `videoSDKJWT` +- Expired JWT +- Using Meeting SDK credentials/signature instead of Video SDK credentials/JWT +- Session name/passcode mismatch + +**Fix**: +- Generate JWT server-side; keep TTL short; check clock skew. +- Log the join errors surfaced by the toolkit callbacks. + +## UI Renders Blank / Cropped + +**Common cause**: The container has no height (common in flex layouts). + +**Fix**: +- Ensure the container has an explicit height (for example `height: 100vh`). + +## "I Need Granular UI Components" + +**Reality**: UI Toolkit is optimized for fast, prebuilt UI. It’s not meant to expose every internal tile/control as a first-class primitive. + +**Fix**: +- If you need fully custom layout, use raw **Zoom Video SDK** and build your own UI. diff --git a/partner-built/zoom-plugin/skills/video-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/video-sdk/RUNBOOK.md new file mode 100644 index 00000000..852c7920 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/RUNBOOK.md @@ -0,0 +1,78 @@ +# Video SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Video SDK failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Product Choice + +- Video SDK is for custom video experiences (not Zoom Meeting UI). +- If you expect native Zoom meeting UI behavior, use Meeting SDK instead. + +## 2) Confirm Lifecycle Order + +Required order: +1. `createClient()` +2. `init()` +3. `join()` +4. `getMediaStream()` +5. `startAudio()` / `startVideo()` + +Calling stream APIs before `join()` causes silent failures. + +## 3) Confirm Token Generation + +- JWT must be generated server-side. +- Validate `app_key`, `role_type`, `tpc`, `iat`, `exp` claims. +- Ensure topic (`tpc`) matches what clients join with. + +## 4) Confirm Rendering Pattern + +- Use event-driven attach/detach flow for participant video. +- Handle user join/leave and peer video state changes. +- Do not assume remote video auto-renders. + +## 5) Confirm Delivery Method + +- npm vs CDN globals differ (`ZoomVideo` vs `WebVideoSDK.default`). +- In CDN/module setups, guard for SDK-load race conditions. + +## 6) Quick Probes + +- Signature endpoint returns valid JWT payload. +- Join succeeds for two users on same `topic`. +- Audio/video start calls return success. +- Browser logs show no mixed-content/CORS blocking. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify signature/token endpoint responds +curl -sS -i "$VIDEO_SDK_BASE_URL/api/signature" + +# 2) Verify app page is reachable +curl -sS -i "$VIDEO_SDK_BASE_URL" +``` + +Expected: JSON from token endpoint and HTML from app route. + +## 7) Fast Decision Tree + +- **No media stream** -> check lifecycle order (`getMediaStream` after `join`). +- **Only local video works** -> missing event-driven remote attach flow. +- **Join auth errors** -> JWT claims mismatch or expired token. + +## 8) SDK Selection Guardrail + +- Use Video SDK for custom video sessions you design. +- Use Meeting SDK for Zoom-native meeting experience embedding. + +## 9) Wrong-Path Detector (SDK vs REST/Meeting SDK) + +- If implementation asks for `meetingNumber` or uses `join_url`, you are not in Video SDK flow. +- If implementation creates resources via `/v2/meetings` for join flow, you are on REST/Meeting path. +- For Video SDK MVP, require: Video SDK JWT + `client.join(topic, ...)` + media stream lifecycle. diff --git a/partner-built/zoom-plugin/skills/video-sdk/SKILL.md b/partner-built/zoom-plugin/skills/video-sdk/SKILL.md new file mode 100644 index 00000000..d6b7542e --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/SKILL.md @@ -0,0 +1,372 @@ +--- +name: build-zoom-video-sdk-app +description: Reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual Zoom meeting. +triggers: + - "custom video" + - "video sdk" + - "build video app" + - "video session" + - "video chat" + - "video call" + - "video conferencing" + - "custom video ui" + - "twitter spaces" + - "clubhouse alternative" + - "audio-only room" + - "screen sharing" + - "virtual background" + - "native video sdk" +--- + +# /build-zoom-video-sdk-app + +Background reference for fully custom video-session products. Prefer `plan-zoom-product` first when the boundary between Meeting SDK and Video SDK is still unclear. + +Build custom video experiences powered by Zoom's infrastructure. + +## Hard Routing Guardrail (Read First) + +- If the user asks for custom real-time video app behavior (topic/session join, custom rendering, attach/detach), route to Video SDK. +- Do not switch to REST meeting endpoints for Video SDK join flows. +- Video SDK does not use Meeting IDs, `join_url`, or Meeting SDK join payload fields (`meetingNumber`, `passWord`). + +## Meeting SDK vs Video SDK + +| Feature | Meeting SDK | Video SDK | +|---------|-------------|-----------| +| UI | Default Zoom UI or Custom UI | **Fully custom UI** (you build it) | +| Experience | Zoom meetings | Video sessions | +| Branding | Limited customization | **Full branding control** | +| Features | Full Zoom features | Core video features | + +## UI Options (Web) + +Video SDK gives you **full control over the UI**: + +| Option | Description | +|--------|-------------| +| **UI Toolkit** | Pre-built React components (low-code) | +| **Custom UI** | Build your own UI using the SDK APIs | + +## Prerequisites + +- Zoom Video SDK credentials from Marketplace +- SDK Key and Secret +- Web development environment + +> **Need help with OAuth or signatures?** See the **[zoom-oauth](../oauth/SKILL.md)** skill for authentication flows. + +> **Need pre-join diagnostics on web?** Use **[probe-sdk](../probe-sdk/SKILL.md)** before Video SDK `join()` to reduce first-minute failures. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](RUNBOOK.md)** before deep debugging. + +## Quick Start (Web) + +### NPM Usage (Bundler like Vite/Webpack) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// IMPORTANT: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### CDN Usage (No Bundler) + +> **WARNING: Ad blockers block `source.zoom.us`**. Self-host the SDK to avoid issues. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-1.12.0.min.js" -o js/zoom-video-sdk.min.js +``` + +```html + +``` + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +// Must use .default property +const ZoomVideo = WebVideoSDK.default; +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// IMPORTANT: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### ES Module with CDN (Race Condition Fix) + +When using ` + +``` + +### Angular + +Add CSS to `angular.json`: +```json +"styles": [ + "node_modules/@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css" +] +``` + +## Quick Start + +```html +
+``` + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const config = { + videoSDKJWT: "your-jwt-token", + sessionName: "SessionA", + userName: "UserA", + sessionPasscode: "abc123", + featuresOptions: { + preview: true, + video: true, + audio: true, + share: true, + chat: true, + users: true, + settings: true, + leave: true, + }, +}; + +const sessionContainer = document.getElementById("sessionContainer"); +uitoolkit.joinSession(sessionContainer, config); +``` + +## Feature Components + +Toggle components on/off via `featuresOptions`: + +| Component | Description | Paid Plan? | +|-----------|-------------|------------| +| `preview` | Pre-session device selection, virtual background | No | +| `video` | Video layout for sending/receiving | No | +| `audio` | Audio button and controls | No | +| `share` | Screen sharing | No | +| `chat` | In-session chat | No | +| `users` | Participant list | No | +| `settings` | Device config, virtual background, stats | No | +| `viewMode` | Grid/speaker view options | No | +| `leave` | Leave/end session button | No | +| `invite` | Invite via link | No | +| `theme` | Theme color selection | No | +| `feedback` | Session feedback form | No | +| `troubleshoot` | Zoom Probe SDK troubleshooting | No | +| `subsession` | Breakout rooms button | No | +| `playback` | Media file playback | No | +| `recording` | Cloud recording | **Yes** | +| `phone` | Join by phone audio | **Yes** | +| `caption` | Live translations | **Yes** | + +## React Example + +```jsx +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; +import { useEffect, useRef } from "react"; + +function VideoSession({ jwt, sessionName, userName }) { + const containerRef = useRef(null); + + useEffect(() => { + const config = { + videoSDKJWT: jwt, + sessionName, + userName, + sessionPasscode: "abc123", + featuresOptions: { + video: true, + audio: true, + chat: true, + users: true, + leave: true, + }, + }; + + if (containerRef.current) { + uitoolkit.joinSession(containerRef.current, config); + } + + return () => { + if (containerRef.current) { + uitoolkit.closeSession(containerRef.current); + } + }; + }, [jwt, sessionName, userName]); + + return
; +} +``` + +## Event Listeners + +```javascript +// Session events +uitoolkit.onSessionJoined(() => { + console.log("Session joined"); +}); + +uitoolkit.onSessionClosed(() => { + console.log("Session closed"); +}); + +// Unsubscribe +uitoolkit.offSessionJoined(callback); +uitoolkit.offSessionClosed(callback); +``` + +## Component Visibility Control + +```javascript +// Hide all components +uitoolkit.hideAllComponents(); + +// Show/hide individual components +uitoolkit.showControlsComponent(container); +uitoolkit.hideControlsComponent(container); + +uitoolkit.showChatComponent(container); +uitoolkit.hideChatComponent(container); + +uitoolkit.showUsersComponent(container); +uitoolkit.hideUsersComponent(container); + +uitoolkit.showSettingsComponent(container); +uitoolkit.hideSettingsComponent(container); +``` + +## Close Session + +```javascript +uitoolkit.closeSession(sessionContainer); +``` + +## Live Demo + +- **With SharedArrayBuffer**: https://videosdk.dev/uitoolkit/ +- **Without SharedArrayBuffer**: https://videosdk.dev/uitoolkit-no-sab/ + +## Sample Repositories + +| Framework | Repository | +|-----------|------------| +| React | [zoom/videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | +| Angular | [zoom/videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | +| Vue.js | [zoom/videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | +| Vanilla JS | [zoom/videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **npm package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **GitHub**: https://github.com/zoom/videosdk-zoom-ui-toolkit-web diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/RUNBOOK.md b/partner-built/zoom-plugin/skills/video-sdk/unity/RUNBOOK.md new file mode 100644 index 00000000..4301886d --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK Unity 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Video SDK custom session flow for Unity (not Meeting SDK). +- Verify UI/state are driven by session events, not meeting semantics. +- Wrapper platforms require JS/native bridge synchronization checks. + +## 2) Confirm Required Credentials + +- Video SDK app credentials (SDK Key/Secret) stored server-side. +- Backend-generated session JWT token. +- Session fields (`sessionName`, `userName`, role type) resolved before join. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK client/context and register event listeners. +2. Generate/fetch session token from backend. +3. Join session and establish media streams. +4. Handle participant/media/control events during active session. + +## 4) Confirm Event/State Handling + +- Keep participant state keyed by user/session IDs. +- Reconcile subscribe/unsubscribe transitions for video/audio/share streams. +- Treat reconnect and device-change events as first-class state transitions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave/end session and release helper/client resources. +- Remove listeners to avoid duplicate callbacks on rejoin. +- Re-check SDK version compatibility before deployment updates. + +## 6) Quick Probes + +- Token issuance and join flow succeed once end-to-end. +- Audio/video publish-subscribe operations complete with expected callbacks. +- Leave/rejoin works without leaked listener or stream state. + +## 7) Fast Decision Tree + +- Join fails immediately -> invalid/expired token or session field mismatch. +- Media state stuck -> listener binding/order issue or permission/device problem. +- Inconsistent behavior after update -> wrapper/native SDK version mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/video-sdk/unity/ +- https://marketplacefront.zoom.us/sdk/custom/unity/index.html + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/video-sdk/unity/` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/unity/` diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/SKILL.md b/partner-built/zoom-plugin/skills/video-sdk/unity/SKILL.md new file mode 100644 index 00000000..b623f01e --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/SKILL.md @@ -0,0 +1,39 @@ +--- +name: zoom-video-sdk-unity +description: | + Zoom Video SDK for Unity wrapper integrations. Use when building custom Unity-based + video session experiences and mapping Unity scene/UI state to Video SDK events. +user-invocable: false +triggers: + - "video sdk unity" + - "zoom unity sdk" + - "unity custom video" + - "unity video session" + - "unity video wrapper" +--- + +# Zoom Video SDK (Unity) + +Use this skill when building Unity apps that integrate Zoom Video SDK wrapper APIs. + +## Start Here + +1. [unity.md](unity.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/session-join-pattern.md](examples/session-join-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/unity-reference-map.md](references/unity-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/unity/index.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/architecture.md b/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/architecture.md new file mode 100644 index 00000000..48f6d101 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/architecture.md @@ -0,0 +1,18 @@ +# Unity Architecture Concept + +```mermaid +flowchart LR + Scene[Unity Scene + UI] --> SessionMgr[Session Manager Script] + SessionMgr --> Wrapper[Zoom Video SDK Unity Wrapper] + SessionMgr --> TokenAPI[Token API] + TokenAPI --> Signer[Server JWT Signer] + Wrapper --> Events[SDK Event Callbacks] + Events --> SessionMgr + SessionMgr --> Scene +``` + +## Design guidance + +- Keep wrapper calls behind one Session Manager abstraction. +- Convert SDK callbacks into explicit Unity state updates. +- Avoid direct credential logic in Unity client. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/lifecycle-workflow.md b/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..7310e742 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/concepts/lifecycle-workflow.md @@ -0,0 +1,21 @@ +# Unity Lifecycle Workflow + +```mermaid +flowchart TD + A[Get token from backend] --> B[Initialize Unity wrapper] + B --> C[Bind SDK events] + C --> D[Join session] + D --> E[Map state to Unity scene/UI] + E --> F[Handle media and participant updates] + F --> G[Leave session] + G --> H[Dispose wrapper/resources] +``` + +## Operational sequence + +1. Request token from backend. +2. Initialize SDK wrapper and event handlers. +3. Join session with topic/session name and display identity. +4. Start/stop local media via wrapper APIs. +5. Apply participant/media updates to Unity scene objects. +6. Cleanly dispose resources on leave or scene switch. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/examples/session-join-pattern.md b/partner-built/zoom-plugin/skills/video-sdk/unity/examples/session-join-pattern.md new file mode 100644 index 00000000..d0c5f766 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/examples/session-join-pattern.md @@ -0,0 +1,21 @@ +# Unity Session Join Pattern + +```csharp +public async Task JoinVideoSession(string sessionName, string userName) +{ + var token = await tokenClient.FetchVideoToken(sessionName, userName); + + zoomSdk.Initialize(initParams); + zoomSdk.OnSessionJoin += HandleSessionJoin; + + zoomSdk.JoinSession(sessionName, userName, token); + + zoomSdk.GetAudioHelper().StartAudio(); + zoomSdk.GetVideoHelper().StartVideo(); +} +``` + +## Notes + +- Bind/unbind Unity event handlers during scene lifecycle. +- Guard against stale handlers when reloading scenes. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/references/environment-variables.md b/partner-built/zoom-plugin/skills/video-sdk/unity/references/environment-variables.md new file mode 100644 index 00000000..aed4f384 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/references/environment-variables.md @@ -0,0 +1,13 @@ +# Unity Environment Variables + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_VIDEO_SDK_KEY` | Yes | Video SDK credential pair (server use) | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Yes (server only) | Token signing secret | Zoom Marketplace -> Video SDK app -> App Credentials | +| `VIDEO_SDK_TOKEN_ENDPOINT` | Yes | Unity app token fetch URL | Your backend deployment config | +| `VIDEO_SDK_SESSION_NAME` | Runtime | Session/topic identifier | Generated by game/app workflow | +| `VIDEO_SDK_USER_NAME` | Runtime | Participant display name | Game/app profile identity | + +## Runtime-only values + +- `VIDEO_SDK_TOKEN` should be short-lived and backend-generated. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/references/unity-reference-map.md b/partner-built/zoom-plugin/skills/video-sdk/unity/references/unity-reference-map.md new file mode 100644 index 00000000..5025bef3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/references/unity-reference-map.md @@ -0,0 +1,18 @@ +# Unity Reference Map + +## Docs anchors + +- Integration docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference index: https://marketplacefront.zoom.us/sdk/custom/unity/index.html + +## API areas to focus on + +- Core SDK wrapper classes +- Session context/init params +- Audio/video helper wrappers +- Delegate/event interfaces for participant/media state + +## Crawl summary + +- Reference pages crawled: 98 +- Docs pages crawled: 6 (5 markdown files persisted) diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/references/versioning-and-compatibility.md b/partner-built/zoom-plugin/skills/video-sdk/unity/references/versioning-and-compatibility.md new file mode 100644 index 00000000..60329698 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# Unity Versioning and Compatibility + +## Package evidence + +- Wrapper package: `unity-zoom-video-sdk-0.0.2-beta.zip` +- Contains `ZoomVideoSDK.unitypackage` wrapper bundle. + +## Compatibility notes + +- Unity wrapper versioning is independent from native Android/iOS/macOS SDK streams. +- Validate each API/event used in wrapper reference docs before implementation. +- Treat wrapper as potentially partial feature surface versus native SDKs. + +## Contradictions or drift to watch + +- Wrapper release (`0.0.2-beta`) is substantially behind native 2.5.0 package family. +- Some docs and feature names may differ between wrapper and native platform references. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/video-sdk/unity/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..bf4b0c5d --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/scenarios/high-level-scenarios.md @@ -0,0 +1,26 @@ +# Unity High-Level Scenarios + +## 1. Interactive virtual events + +- Unity scene-based sessions with branded interaction elements. +- Runtime participant controls and custom overlays. + +## 2. Training simulators with live instructor + +- Instructor-led sessions embedded into simulation environments. +- Tokenized joins mapped to training cohorts. + +## 3. Support walkthrough spaces + +- Agent + customer co-presence in guided Unity environments. +- Scene state tied to session participant roles. + +## 4. Product demo theaters + +- Host-controlled interactive demo rooms. +- Event callbacks drive stage, spotlight, and audience states. + +## 5. Experimental mixed-reality collaboration + +- Session media integrated with custom rendering pipelines. +- Explicit fallback when wrapper feature parity is incomplete. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/video-sdk/unity/troubleshooting/common-issues.md new file mode 100644 index 00000000..5660dde1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Unity Common Issues + +## Missing API from native docs + +- Confirm API exists in Unity wrapper reference, not just native SDK docs. +- Implement fallback logic for unsupported wrapper features. + +## Join/session event issues + +- Verify token validity and wrapper event binding order. +- Ensure scene lifecycle does not drop active handlers. + +## Platform build permission problems + +- Confirm microphone/camera permissions for target platform build settings. +- Re-test on clean devices with fresh permission prompts. + +## Rendering/state mismatch + +- Keep scene state synchronized from SDK events. +- Reset participant objects on leave/rejoin transitions. diff --git a/partner-built/zoom-plugin/skills/video-sdk/unity/unity.md b/partner-built/zoom-plugin/skills/video-sdk/unity/unity.md new file mode 100644 index 00000000..86205a5d --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/unity/unity.md @@ -0,0 +1,31 @@ +# Unity Video SDK Overview + +## What this platform skill is for + +- Integrating Zoom Video SDK wrapper into Unity projects +- Building custom in-scene video interaction experiences +- Mapping session lifecycle and participant events into Unity game loop/UI + +## Primary implementation path + +1. Backend generates short-lived Video SDK token. +2. Unity initializes wrapper objects and event bindings. +3. Unity joins session with session name and user identity. +4. Unity updates scene/UI based on SDK callbacks. +5. Unity leaves session and disposes wrapper resources cleanly. + +## Prerequisites + +- Unity project with packaged `ZoomVideoSDK.unitypackage` +- Backend token endpoint +- Platform-specific permissions for mic/camera on target builds + +## Important notes + +- Unity wrapper version may lag native SDK versions. +- Validate feature availability before promising parity with native platforms. + +## Source links + +- Docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/unity/index.html diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/RUNBOOK.md b/partner-built/zoom-plugin/skills/video-sdk/web/RUNBOOK.md new file mode 100644 index 00000000..659e5f39 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/RUNBOOK.md @@ -0,0 +1,69 @@ +# Video SDK Web 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Video SDK custom session flow for Web (not Meeting SDK). +- Verify UI/state are driven by session events, not meeting semantics. +- Wrapper platforms require JS/native bridge synchronization checks. + +## 2) Confirm Required Credentials + +- Video SDK app credentials (SDK Key/Secret) stored server-side. +- Backend-generated session JWT token. +- Session fields (`sessionName`, `userName`, role type) resolved before join. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK client/context and register event listeners. +2. Generate/fetch session token from backend. +3. Join session and establish media streams. +4. Handle participant/media/control events during active session. + +## 4) Confirm Event/State Handling + +- Keep participant state keyed by user/session IDs. +- Reconcile subscribe/unsubscribe transitions for video/audio/share streams. +- Treat reconnect and device-change events as first-class state transitions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave/end session and release helper/client resources. +- Remove listeners to avoid duplicate callbacks on rejoin. +- Re-check SDK version compatibility before deployment updates. + +## 6) Quick Probes + +- Token issuance and join flow succeed once end-to-end. +- Audio/video publish-subscribe operations complete with expected callbacks. +- Leave/rejoin works without leaked listener or stream state. + +## 7) Fast Decision Tree + +- Join fails immediately -> invalid/expired token or session field mismatch. +- Media state stuck -> listener binding/order issue or permission/device problem. +- Inconsistent behavior after update -> wrapper/native SDK version mismatch. + +## 8) Source Checkpoints + +## 9) Field Pitfalls (Custom Flows) + +- For waiting-room -> main-session transfers and browser/CSP edge cases, review: + - `troubleshooting/common-issues.md` (section: "Real-World Integration Pitfalls (Custom Waiting Room Flows)") + +### Official docs + +- https://developers.zoom.us/docs/video-sdk/web/ +- https://marketplacefront.zoom.us/sdk/custom/web/modules.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/video-sdk/web/` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/web/` diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/SKILL.md b/partner-built/zoom-plugin/skills/video-sdk/web/SKILL.md new file mode 100644 index 00000000..99c53425 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/SKILL.md @@ -0,0 +1,821 @@ +--- +name: video-sdk/web +description: "Zoom Video SDK for Web - JavaScript/TypeScript integration for browser-based video sessions, real-time communication, screen sharing, recording, and live transcription" +user-invocable: false +triggers: + - "video sdk web" + - "custom video web" + - "attachvideo" + - "peer-video-state-change" + - "web videosdk" +--- + +# Zoom Video SDK - Web Development + +Expert guidance for developing with the Zoom Video SDK on Web. This SDK enables custom video applications in the browser with real-time video/audio, screen sharing, cloud recording, live streaming, chat, and live transcription. + +This skill is for **custom video sessions**, not embedded Zoom meetings. +If the user wants a custom UI for a real Zoom meeting, route to +[../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md). + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/web/modules.html +**Sample Repository**: https://github.com/zoom/videosdk-web-sample + +## Quick Links + +**New to Video SDK? Follow this path:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +2. **[Session Join Pattern](examples/session-join-pattern.md)** - Complete working code to join a session +3. **[Video Rendering](examples/video-rendering.md)** - Display video with attachVideo() +4. **[Event Handling](examples/event-handling.md)** - Required events for video/audio + +**Reference:** +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 4-level SDK navigation map +- **[API Reference](references/web-reference.md)** - Methods, events, error codes +- **[SKILL.md](SKILL.md)** - Complete documentation navigation +- **[../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md)** - Optional browser/device/network readiness diagnostics before join + +**Having issues?** +- Video not showing → [Video Rendering](examples/video-rendering.md) (use attachVideo, not renderVideo) +- getMediaStream() returns undefined → Call AFTER join() completes +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) + +## SDK Overview + +The Zoom Video SDK for Web is a JavaScript library that provides: +- **Session Management**: Join/leave video SDK sessions +- **Video/Audio**: Start/stop camera and microphone +- **Screen Sharing**: Share screens or browser tabs +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints +- **Chat**: In-session messaging +- **Command Channel**: Custom command messaging +- **Live Transcription**: Real-time speech-to-text +- **Subsessions**: Breakout room support +- **Whiteboard**: Collaborative whiteboard features +- **Virtual Background**: Blur or custom image backgrounds + +## Prerequisites + +### System Requirements + +- **Modern Browser**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+ +- **Video SDK Credentials**: SDK Key and Secret from [Marketplace](https://marketplace.zoom.us/) +- **JWT Token**: Server-side generated signature + +### Browser Feature Requirements + +```javascript +// Check browser compatibility before init +const compatibility = ZoomVideo.checkSystemRequirements(); +console.log('Audio:', compatibility.audio); +console.log('Video:', compatibility.video); +console.log('Screen:', compatibility.screen); + +// Check feature support +const features = ZoomVideo.checkFeatureRequirements(); +console.log('Supported:', features.supportFeatures); +console.log('Unsupported:', features.unSupportFeatures); +``` + +### Optional Pre-Join Diagnostics (Recommended for Reliability) + +Use Probe SDK as a readiness gate before `client.join(...)` when you need to reduce failed starts: + +1. Run diagnostics with [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md). +2. Evaluate policy (`allow`, `warn`, `block`). +3. Start Video SDK join only when policy allows. + +Cross-skill flow: [../../general/use-cases/probe-sdk-preflight-readiness-gate.md](../../general/use-cases/probe-sdk-preflight-readiness-gate.md) + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/videosdk +``` + +```javascript +import ZoomVideo from '@zoom/videosdk'; +``` + +### CDN (Fallback Strategy Recommended) + +> **Note**: Some networks/ad blockers can block `source.zoom.us`. If you see flaky loads, first try allowlisting the domain in your environment. If needed, consider a fallback (mirror/self-host) only if it's permitted for your use case and you can keep versions in sync. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-2.3.12.min.js" -o public/js/zoom-video-sdk.min.js +``` + +```html + + +``` + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +const ZoomVideo = WebVideoSDK.default; +``` + +## Quick Start + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +// 1. Create client (singleton - returns same instance) +const client = ZoomVideo.createClient(); + +// 2. Initialize SDK +await client.init('en-US', 'Global', { patchJsMedia: true }); + +// 3. Join session +await client.join(topic, signature, userName, password); + +// 4. CRITICAL: Get stream AFTER join +const stream = client.getMediaStream(); + +// 5. Start media +await stream.startVideo(); +await stream.startAudio(); + +// 6. Attach video to DOM +const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P); +document.getElementById('video-container').appendChild(videoElement); +``` + +## SDK Lifecycle (CRITICAL ORDER) + +The SDK has a strict lifecycle. Violating it causes **silent failures**. + +``` +1. Create client: client = ZoomVideo.createClient() +2. Initialize: await client.init('en-US', 'Global', options) +3. Join session: await client.join(topic, signature, userName, password) +4. Get stream: stream = client.getMediaStream() ← ONLY AFTER JOIN +5. Start media: await stream.startVideo() / await stream.startAudio() +``` + +**Common Mistake:** + +```javascript +// WRONG: Getting stream before joining +const stream = client.getMediaStream(); // Returns undefined! +await client.join(...); + +// CORRECT: Get stream after joining +await client.join(...); +const stream = client.getMediaStream(); // Works! +``` + +## Critical Gotchas and Best Practices + +### getMediaStream() ONLY Works After join() + +The #1 issue that causes video/audio to fail: + +```javascript +// WRONG +const stream = client.getMediaStream(); // undefined! +await client.join(...); + +// CORRECT +await client.join(...); +const stream = client.getMediaStream(); // Works +``` + +### Use attachVideo() NOT renderVideo() + +`renderVideo()` is **deprecated**. Use `attachVideo()` which returns a VideoPlayer element: + +```javascript +import { VideoQuality } from '@zoom/videosdk'; + +// CORRECT: attachVideo returns element to append +const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P); +document.getElementById('video-container').appendChild(videoElement); + +// WRONG: renderVideo is deprecated +await stream.renderVideo(canvas, userId, ...); // Don't use! +``` + +### Video Rendering is Event-Driven (CRITICAL) + +You MUST listen for events to properly render participant videos: + +```javascript +// When another participant's video state changes +client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + + if (action === 'Start') { + // Participant turned on video - attach it + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + container.appendChild(element); + } else if (action === 'Stop') { + // Participant turned off video - detach it + await stream.detachVideo(userId); + } +}); + +// When participants join/leave +client.on('user-added', (payload) => { + // New participant joined - check if their video is on + const users = client.getAllUser(); + // Render videos for users with bVideoOn === true +}); + +client.on('user-removed', (payload) => { + // Participant left - clean up their video element + stream.detachVideo(payload[0].userId); +}); +``` + +### Peer Video on Mid-Session Join + +**Existing participants' videos won't auto-render when you join mid-session.** + +```javascript +// After joining, render existing participants' videos +const renderExistingVideos = async () => { + await new Promise(resolve => setTimeout(resolve, 500)); + + const users = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const user of users) { + if (user.bVideoOn && user.userId !== currentUserId) { + const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P); + document.getElementById(`video-${user.userId}`).appendChild(element); + } + } +}; +``` + +### CDN Race Condition with ES Modules + +When using ` + + +``` + +### Nuxt 3 Plugin (Client-Only) + +```typescript +// plugins/videosdk.client.ts +import ZoomVideo from '@zoom/videosdk'; + +export default defineNuxtPlugin(() => { + return { + provide: { + zoomVideo: ZoomVideo, + }, + }; +}); +``` + +```vue + + +``` + +--- + +## Zoom For Government (ZFG) + +If using Zoom For Government, you need a separate SDK key from [marketplace.zoomgov.com](https://marketplace.zoomgov.com/). + +### Option 1: ZFG-Specific Package Version + +```json +{ + "dependencies": { + "@zoom/videosdk": "1.11.0-zfg" + } +} +``` + +```javascript +client.init('en-US', 'Global'); +``` + +### Option 2: Custom WebEndpoint + +```javascript +client.init('en-US', 'https://source.zoomgov.com/videosdk/1.11.0/lib', { + webEndpoint: 'www.zoomgov.com', +}); +``` + +--- + +## Official Sample Repositories + +| Framework | Repository | Branch | +|-----------|------------|--------| +| Vanilla JS/TS | [videosdk-web-sample](https://github.com/zoom/videosdk-web-sample) | master | +| React | [videosdk-react](https://github.com/zoom/videosdk-react) | main | +| Next.js (App) | [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | app-router | +| Next.js (Pages) | [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | pages-router | +| Vue/Nuxt | [videosdk-vue-nuxt-quickstart](https://github.com/zoom/videosdk-vue-nuxt-quickstart) | main | +| UI Toolkit (React) | [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | main | +| Auth Endpoint | [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | main | + +--- + +## Common Patterns Across Frameworks + +### 1. Client-Side Only + +The SDK must run client-side. All frameworks need to handle this: + +| Framework | Solution | +|-----------|----------| +| Next.js | `'use client'` or `dynamic(..., { ssr: false })` | +| Nuxt 3 | `.client.ts` plugin or `` | +| Vue SPA | No special handling needed | + +### 2. Lifecycle Management + +```typescript +// Always follow this order: +const client = ZoomVideo.createClient(); +await client.init(...); +await client.join(...); +const stream = client.getMediaStream(); // ONLY after join() + +// Cleanup on unmount +await client.leave(); +ZoomVideo.destroyClient(); +``` + +### 3. Event-Driven Video Rendering + +```typescript +// All frameworks should use this pattern +client.on('peer-video-state-change', async ({ action, userId }) => { + if (action === 'Start') { + const el = await stream.attachVideo(userId, VideoQuality.Video_360P); + container.appendChild(el); + } else { + await stream.detachVideo(userId); + } +}); +``` + +## Related Documentation + +- [React Hooks](react-hooks.md) - @zoom/videosdk-react +- [Session Join](session-join-pattern.md) - Core SDK patterns +- [Video Rendering](video-rendering.md) - attachVideo() details diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/react-hooks.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/react-hooks.md new file mode 100644 index 00000000..f89ae97f --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/react-hooks.md @@ -0,0 +1,411 @@ +# React Hooks (@zoom/videosdk-react) + +Official React SDK that provides custom hooks and components for integrating Zoom Video SDK into React apps. + +## Installation + +```bash +npm install @zoom/videosdk +npm install https://github.com/zoom/videosdk-react/releases/download/v0.0.1/zoom-videosdk-react-0.0.1.tgz +``` + +**Prerequisites:** +- React 18+ +- Zoom Video SDK account and credentials + +## Quick Start + +```tsx +import { + useSession, + useSessionUsers, + VideoPlayerComponent, + VideoPlayerContainerComponent +} from '@zoom/videosdk-react'; + +function VideoChat() { + const { isInSession, isLoading, isError } = useSession( + "session123", + "your_jwt_token", + "User Name" + ); + + const participants = useSessionUsers(); + + if (isLoading) return
Joining session...
; + if (isError) return
Error joining session
; + + return ( +
+ {isInSession && ( + + {participants.map(participant => ( + + ))} + + )} +
+ ); +} +``` + +## Available Hooks + +### useSession + +Manages the complete lifecycle of a Zoom video session. + +```tsx +const { isInSession, isLoading, isError, error } = useSession( + topic, // Session topic/ID + token, // JWT authentication token + userName, // Display name + sessionPassword, // Optional session password + sessionIdleTimeoutMins, // Optional idle timeout + { + disableVideo: false, + disableAudio: false, + language: "en-US", + dependentAssets: "Global", + waitBeforeJoining: 0, // Delay before auto-joining + endSessionOnLeave: false, // End session when host leaves + } +); +``` + +**Return values:** +| Field | Type | Description | +|-------|------|-------------| +| `isInSession` | boolean | Currently in session | +| `isLoading` | boolean | Session join in progress | +| `isError` | boolean | Error occurred | +| `error` | Error | Error object if any | + +### useSessionUsers + +Provides real-time access to all session participants with reference stability. + +```tsx +const participants = useSessionUsers(); + +// participants is an array of Participant objects +participants.map(p => ( +
+ {p.displayName} - {p.bVideoOn ? 'Video On' : 'Video Off'} +
+)); +``` + +### useMyself + +Access the local user in the current session. + +```tsx +const myself = useMyself(); + +return ( +
+ {myself.userName} - {myself.bVideoOn ? 'Video On' : 'Video Off'} +
+); +``` + +### useScreenShareUsers + +Get users who are currently sharing their screen. + +```tsx +const screenshareusers = useScreenShareUsers(); + + + {screenshareusers.map(userId => ( + + ))} + +``` + +### useVideoState + +Manages video capture state and controls. + +```tsx +const { isVideoOn, toggleVideo, setVideo } = useVideoState(); + +// Toggle video on/off + + +// Set video state explicitly + +``` + +### useAudioState + +Comprehensive audio state management. + +```tsx +const { + isAudioMuted, + isCapturingAudio, + toggleMute, + toggleCapture, + setMute, + setCapture +} = useAudioState(); + +// Toggle mute + + +// Toggle audio capture + +``` + +### useScreenshare + +Manages screen sharing functionality. + +```tsx +const { ScreenshareRef, startScreenshare } = useScreenshare(); + +return ( +
+ + +
+); +``` + +## Components + +### VideoPlayerContainerComponent + +**Required container** for video players. Must wrap all `VideoPlayerComponent` instances. + +```tsx + + {participants.map(participant => ( + + ))} + +``` + +### VideoPlayerComponent + +Renders individual participant video streams. + +```tsx +const participants = useSessionUsers(); + + +``` + +### ScreenShareContainerComponent + +**Required container** for screen share players. + +```tsx + + {screenshareusers.map(userId => ( + + ))} + +``` + +### ScreenSharePlayerComponent + +Renders screen share streams. + +```tsx + +``` + +## Complete Example + +```tsx +import React from 'react'; +import { + useSession, + useSessionUsers, + useMyself, + useVideoState, + useAudioState, + useScreenshare, + useScreenShareUsers, + VideoPlayerComponent, + VideoPlayerContainerComponent, + ScreenSharePlayerComponent, + ScreenShareContainerComponent, + LocalScreenShareComponent +} from '@zoom/videosdk-react'; + +interface VideoCallProps { + topic: string; + token: string; + userName: string; +} + +export const VideoCall: React.FC = ({ topic, token, userName }) => { + // Session management + const { isInSession, isLoading, isError, error } = useSession( + topic, + token, + userName, + undefined, // no password + undefined, // default idle timeout + { + disableVideo: false, + disableAudio: false, + } + ); + + // Participants + const participants = useSessionUsers(); + const myself = useMyself(); + const screenshareUsers = useScreenShareUsers(); + + // Media controls + const { isVideoOn, toggleVideo } = useVideoState(); + const { isAudioMuted, isCapturingAudio, toggleMute, toggleCapture } = useAudioState(); + const { ScreenshareRef, startScreenshare } = useScreenshare(); + + // Loading state + if (isLoading) { + return
Joining session...
; + } + + // Error state + if (isError) { + return
Error: {error?.message}
; + } + + // Not in session + if (!isInSession) { + return
Not in session
; + } + + return ( +
+ {/* Video Grid */} + + {participants.map(participant => ( +
+ +
{participant.displayName}
+
+ ))} +
+ + {/* Screen Share */} + {screenshareUsers.length > 0 && ( + + {screenshareUsers.map(userId => ( + + ))} + + )} + + {/* Local Screen Share Preview */} + + + {/* Controls */} +
+ {/* Audio */} + {!isCapturingAudio ? ( + + ) : ( + + )} + + {/* Video */} + + + {/* Screen Share */} + +
+ + {/* Participant Info */} +
+

Logged in as: {myself?.userName}

+

Participants: {participants.length}

+
+
+ ); +}; +``` + +## Interoperability with @zoom/videosdk + +The React SDK is designed to work alongside the core `@zoom/videosdk`. You can use both: + +```tsx +import ZoomVideo from '@zoom/videosdk'; +import { useSession, useSessionUsers } from '@zoom/videosdk-react'; + +// Use React hooks for common patterns +const { isInSession } = useSession(topic, token, userName); +const participants = useSessionUsers(); + +// Access the underlying client for advanced features +const client = ZoomVideo.createClient(); +const chatClient = client.getChatClient(); +const recordingClient = client.getRecordingClient(); +``` + +## Project Structure + +``` +src/ +├── components/ # React components +│ ├── VideoPlayerComponent +│ ├── VideoPlayerContainerComponent +│ ├── ScreenSharePlayerComponent +│ ├── ScreenShareContainerComponent +│ └── LocalScreenShareComponent +├── hooks/ # Custom React hooks +│ ├── useSession +│ ├── useSessionUsers +│ ├── useMyself +│ ├── useVideoState +│ ├── useAudioState +│ ├── useScreenshare +│ └── useScreenShareUsers +└── index.ts # Main exports +``` + +## Key Benefits + +| Benefit | Description | +|---------|-------------| +| **Simplified State** | Automatic participant state management | +| **Reference Stability** | Hooks maintain stable references | +| **TypeScript Support** | Full type definitions included | +| **Flexible** | Use alongside core SDK | +| **Customizable** | Components accept standard React props | + +## Official Repository + +- **GitHub**: [zoom/videosdk-react](https://github.com/zoom/videosdk-react) + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Manual SDK usage +- [Video Rendering](video-rendering.md) - Manual attachVideo() patterns +- [Event Handling](event-handling.md) - Event patterns diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/recording.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/recording.md new file mode 100644 index 00000000..9881b1f5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/recording.md @@ -0,0 +1,435 @@ +# Cloud Recording + +Complete guide to controlling cloud recording in the Zoom Video SDK for Web. + +## Prerequisites + +- Cloud recording must be enabled on your Zoom account +- User must have recording privileges (typically host/manager) + +## Getting the Recording Client + +```javascript +// Get recording client after joining session +const recordingClient = client.getRecordingClient(); +``` + +## Basic Operations + +### Check Recording Capability + +```javascript +// Check if cloud recording is available +const canRecord = recordingClient.canStartRecording(); + +if (!canRecord) { + console.log('Cloud recording not enabled for this session'); +} +``` + +### Start Recording + +```javascript +try { + await recordingClient.startCloudRecording(); + console.log('Recording started'); +} catch (error) { + console.error('Failed to start recording:', error); +} +``` + +### Stop Recording + +```javascript +try { + await recordingClient.stopCloudRecording(); + console.log('Recording stopped'); +} catch (error) { + console.error('Failed to stop recording:', error); +} +``` + +### Pause/Resume Recording + +```javascript +// Pause +await recordingClient.pauseCloudRecording(); + +// Resume +await recordingClient.resumeCloudRecording(); +``` + +### Get Recording Status + +```javascript +import { RecordingStatus } from '@zoom/videosdk'; + +const status = recordingClient.getCloudRecordingStatus(); + +switch (status) { + case RecordingStatus.Recording: + console.log('Currently recording'); + break; + case RecordingStatus.Paused: + console.log('Recording paused'); + break; + case RecordingStatus.Stopped: + console.log('Not recording'); + break; +} +``` + +## Recording Events + +### Recording State Changes + +```javascript +client.on('recording-change', (payload) => { + const { state } = payload; + + switch (state) { + case 'Recording': + showRecordingIndicator(); + break; + case 'Paused': + showPausedIndicator(); + break; + case 'Stopped': + hideRecordingIndicator(); + break; + } +}); +``` + +### Individual Recording Consent + +When individual recording is enabled, participants must consent: + +```javascript +client.on('individual-recording-change', (payload) => { + const { state, userId } = payload; + + switch (state) { + case 'Ask': + // Host is asking you to accept individual recording + showRecordingConsentDialog(); + break; + case 'Accept': + // User accepted recording + console.log('User', userId, 'accepted recording'); + break; + case 'Decline': + // User declined recording + console.log('User', userId, 'declined recording'); + break; + } +}); +``` + +### Respond to Recording Consent + +```javascript +// Accept individual recording +await recordingClient.acceptIndividualRecording(); + +// Decline individual recording +await recordingClient.declineIndividualRecording(); +``` + +## Complete Recording Manager + +```javascript +import { RecordingStatus } from '@zoom/videosdk'; + +class RecordingManager { + constructor(client) { + this.client = client; + this.recordingClient = null; + this.currentStatus = RecordingStatus.Stopped; + this.onStatusChange = null; + this.onConsentRequired = null; + } + + init() { + this.recordingClient = this.client.getRecordingClient(); + this.currentStatus = this.recordingClient.getCloudRecordingStatus(); + this.setupEventListeners(); + } + + setupEventListeners() { + // Recording state changes + this.client.on('recording-change', (payload) => { + this.currentStatus = payload.state; + + if (this.onStatusChange) { + this.onStatusChange(payload.state); + } + }); + + // Individual recording consent + this.client.on('individual-recording-change', (payload) => { + if (payload.state === 'Ask' && this.onConsentRequired) { + this.onConsentRequired(); + } + }); + } + + canStartRecording() { + return this.recordingClient.canStartRecording(); + } + + isRecording() { + return this.currentStatus === 'Recording' || + this.currentStatus === RecordingStatus.Recording; + } + + isPaused() { + return this.currentStatus === 'Paused' || + this.currentStatus === RecordingStatus.Paused; + } + + async start() { + if (!this.canStartRecording()) { + throw new Error('Cloud recording not available'); + } + + if (this.isRecording()) { + throw new Error('Already recording'); + } + + await this.recordingClient.startCloudRecording(); + } + + async stop() { + if (!this.isRecording() && !this.isPaused()) { + throw new Error('Not currently recording'); + } + + await this.recordingClient.stopCloudRecording(); + } + + async pause() { + if (!this.isRecording()) { + throw new Error('Not currently recording'); + } + + await this.recordingClient.pauseCloudRecording(); + } + + async resume() { + if (!this.isPaused()) { + throw new Error('Recording not paused'); + } + + await this.recordingClient.resumeCloudRecording(); + } + + async acceptConsent() { + await this.recordingClient.acceptIndividualRecording(); + } + + async declineConsent() { + await this.recordingClient.declineIndividualRecording(); + } + + getStatus() { + return this.recordingClient.getCloudRecordingStatus(); + } +} + +// Usage +const recordingManager = new RecordingManager(client); +recordingManager.init(); + +recordingManager.onStatusChange = (status) => { + updateRecordingUI(status); +}; + +recordingManager.onConsentRequired = () => { + showConsentDialog({ + onAccept: () => recordingManager.acceptConsent(), + onDecline: () => recordingManager.declineConsent(), + }); +}; + +// UI handlers +document.getElementById('start-recording').onclick = async () => { + try { + await recordingManager.start(); + } catch (error) { + alert(error.message); + } +}; + +document.getElementById('stop-recording').onclick = async () => { + try { + await recordingManager.stop(); + } catch (error) { + alert(error.message); + } +}; +``` + +## React Component + +```typescript +import React, { useState, useEffect } from 'react'; +import { VideoClient, RecordingStatus } from '@zoom/videosdk'; + +interface RecordingControlsProps { + client: typeof VideoClient; + isHost: boolean; +} + +export const RecordingControls: React.FC = ({ + client, + isHost +}) => { + const [status, setStatus] = useState(RecordingStatus.Stopped); + const [canRecord, setCanRecord] = useState(false); + const [showConsent, setShowConsent] = useState(false); + + const recordingClient = client.getRecordingClient(); + + useEffect(() => { + // Initial state + setStatus(recordingClient.getCloudRecordingStatus()); + setCanRecord(recordingClient.canStartRecording()); + + // Listen for changes + const handleRecordingChange = (payload: { state: RecordingStatus }) => { + setStatus(payload.state); + }; + + const handleIndividualRecording = (payload: { state: string }) => { + if (payload.state === 'Ask') { + setShowConsent(true); + } + }; + + client.on('recording-change', handleRecordingChange); + client.on('individual-recording-change', handleIndividualRecording); + + return () => { + client.off('recording-change', handleRecordingChange); + client.off('individual-recording-change', handleIndividualRecording); + }; + }, [client, recordingClient]); + + const startRecording = async () => { + try { + await recordingClient.startCloudRecording(); + } catch (error) { + console.error('Start recording failed:', error); + } + }; + + const stopRecording = async () => { + try { + await recordingClient.stopCloudRecording(); + } catch (error) { + console.error('Stop recording failed:', error); + } + }; + + const pauseRecording = async () => { + try { + await recordingClient.pauseCloudRecording(); + } catch (error) { + console.error('Pause recording failed:', error); + } + }; + + const resumeRecording = async () => { + try { + await recordingClient.resumeCloudRecording(); + } catch (error) { + console.error('Resume recording failed:', error); + } + }; + + const acceptConsent = async () => { + await recordingClient.acceptIndividualRecording(); + setShowConsent(false); + }; + + const declineConsent = async () => { + await recordingClient.declineIndividualRecording(); + setShowConsent(false); + }; + + const isRecording = status === RecordingStatus.Recording; + const isPaused = status === RecordingStatus.Paused; + + return ( +
+ {/* Recording indicator */} + {(isRecording || isPaused) && ( +
+ + {isPaused ? 'Recording Paused' : 'Recording'} +
+ )} + + {/* Host controls */} + {isHost && canRecord && ( +
+ {!isRecording && !isPaused && ( + + )} + + {isRecording && ( + <> + + + + )} + + {isPaused && ( + <> + + + + )} +
+ )} + + {/* Consent dialog */} + {showConsent && ( +
+

The host would like to record this session. Do you consent?

+ + +
+ )} +
+ ); +}; +``` + +## Key Points + +1. **Check `canStartRecording()` first** - Recording must be enabled on the account +2. **Only host/manager can control recording** - Regular participants can only consent +3. **Handle consent for individual recording** - Listen to `individual-recording-change` +4. **Show recording indicator** - Let participants know they're being recorded +5. **Recordings are available in Zoom portal** - After session ends, recordings appear in account + +## Error Handling + +```javascript +try { + await recordingClient.startCloudRecording(); +} catch (error) { + if (error.type === 'INVALID_OPERATION') { + // Recording not available or already recording + } else if (error.type === 'INSUFFICIENT_PRIVILEGES') { + // User doesn't have permission to record + } +} +``` + +## Related Documentation + +- [Event Handling](event-handling.md) - Recording events +- [API Reference](../references/web-reference.md) - Full RecordingClient API diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/screen-share.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/screen-share.md new file mode 100644 index 00000000..c19bcdb7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/screen-share.md @@ -0,0 +1,433 @@ +# Screen Share + +Complete guide to sending and receiving screen shares in the Zoom Video SDK for Web. + +## Overview + +Screen sharing involves two distinct operations: +1. **Sending** - Sharing your screen to others (`startShareScreen`) +2. **Receiving** - Viewing others' shared screens (`startShareView` or `attachShareView`) + +## Sending Screen Share + +### Basic Screen Share + +```javascript +// Get the media stream +const stream = client.getMediaStream(); + +// Create a canvas or video element to render the share preview +const shareCanvas = document.getElementById('share-canvas'); + +// Start sharing +await stream.startShareScreen(shareCanvas); +``` + +### With Options + +```javascript +await stream.startShareScreen(shareCanvas, { + displaySurface: 'monitor', // 'monitor' | 'window' | 'browser' + audio: true, // Share system/tab audio + optimizedForSharedVideo: true, // Optimize for video content +}); +``` + +### Stop Sharing + +```javascript +await stream.stopShareScreen(); +``` + +### Pause/Resume + +```javascript +// Pause +await stream.pauseShareScreen(); + +// Resume +await stream.resumeShareScreen(); +``` + +## Receiving Screen Share + +### Method 1: Canvas-based (Legacy) + +```javascript +// Create a canvas for rendering +const receiveCanvas = document.getElementById('share-view-canvas'); + +// Listen for active share changes +client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + + if (state === 'Active') { + // Someone started sharing - render it + await stream.startShareView(receiveCanvas, userId); + } else if (state === 'Inactive') { + // Sharing stopped + await stream.stopShareView(); + } +}); +``` + +### Method 2: VideoPlayer-based (Recommended - SDK 2.2.10+) + +```javascript +// Listen for active share changes +client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + + if (state === 'Active') { + // Attach share view - returns a VideoPlayer element + const element = await stream.attachShareView(userId); + document.getElementById('share-container').appendChild(element); + } else if (state === 'Inactive') { + // Detach share view + const elements = await stream.detachShareView(userId); + if (Array.isArray(elements)) { + elements.forEach(e => e.remove()); + } else if (elements) { + elements.remove(); + } + } +}); +``` + +## Complete Example + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +class ScreenShareManager { + constructor(client) { + this.client = client; + this.stream = null; + this.isSharing = false; + this.activeShareUserId = null; + } + + init() { + this.stream = this.client.getMediaStream(); + this.setupEventListeners(); + } + + setupEventListeners() { + // When someone starts/stops sharing + this.client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + console.log('Share change:', state, 'from user:', userId); + + if (state === 'Active') { + this.activeShareUserId = userId; + await this.renderReceivedShare(userId); + } else { + await this.stopRenderingShare(); + this.activeShareUserId = null; + } + }); + + // When share content changes (e.g., user switches windows) + this.client.on('share-content-change', (payload) => { + console.log('Share content changed for user:', payload.userId); + }); + + // When share dimension changes + this.client.on('share-content-dimension-change', (payload) => { + console.log('Share dimension:', payload.width, 'x', payload.height); + // Resize container if needed + }); + + // When you're forced to stop sharing + this.client.on('passively-stop-share', (payload) => { + console.log('Forced to stop sharing. Reason:', payload); + this.isSharing = false; + }); + + // Share privilege changes + this.client.on('share-privilege-change', (payload) => { + console.log('Share privilege:', payload.privilege); + }); + } + + async startSharing() { + // Check privilege first + const privilege = this.stream.getSharePrivilege(); + if (privilege === 0) { + throw new Error('Sharing not allowed - host has disabled it'); + } + + // Check if share is locked + if (this.stream.isShareLocked()) { + throw new Error('Sharing is locked by host'); + } + + const canvas = document.getElementById('share-preview-canvas'); + + try { + await this.stream.startShareScreen(canvas, { + audio: true, + }); + this.isSharing = true; + console.log('Screen share started'); + } catch (error) { + console.error('Failed to start screen share:', error); + + if (error.type === 'INVALID_OPERATION' && error.reason?.includes('extension')) { + // Chrome extension required (legacy browsers) + console.log('Install extension from:', error.extensionUrl); + } + + throw error; + } + } + + async stopSharing() { + if (!this.isSharing) return; + + await this.stream.stopShareScreen(); + this.isSharing = false; + console.log('Screen share stopped'); + } + + async renderReceivedShare(userId) { + const container = document.getElementById('share-view-container'); + + try { + // Use attachShareView for VideoPlayer-based rendering (SDK 2.2.10+) + const element = await this.stream.attachShareView(userId); + container.innerHTML = ''; + container.appendChild(element); + container.style.display = 'block'; + } catch (error) { + console.error('Failed to render share view:', error); + } + } + + async stopRenderingShare() { + if (!this.activeShareUserId) return; + + const container = document.getElementById('share-view-container'); + container.style.display = 'none'; + + try { + await this.stream.detachShareView(this.activeShareUserId); + container.innerHTML = ''; + } catch (error) { + console.error('Failed to detach share view:', error); + } + } + + // Get who is currently sharing + getShareUserList() { + return this.stream.getShareUserList(); + } + + // Get active sharer's user ID + getActiveShareUserId() { + return this.stream.getActiveShareUserId(); + } +} + +// Usage +const shareManager = new ScreenShareManager(client); +shareManager.init(); + +// Start sharing +document.getElementById('share-btn').onclick = () => shareManager.startSharing(); +document.getElementById('stop-share-btn').onclick = () => shareManager.stopSharing(); +``` + +## Share with Audio + +```javascript +// Share screen with system/tab audio +await stream.startShareScreen(canvas, { + audio: true, +}); + +// Check share audio status +const audioStatus = stream.getShareAudioStatus(); +console.log('Share audio:', audioStatus); + +// Mute/unmute share audio +await stream.muteShareAudio(); // Mute +await stream.unmuteShareAudio(); // Unmute +``` + +## Share Privilege Management (Host Only) + +```javascript +import { SharePrivilege } from '@zoom/videosdk'; + +// Get current privilege +const privilege = stream.getSharePrivilege(); + +// Set privilege (host only) +await stream.setSharePrivilege(SharePrivilege.Unlocked); // Anyone can share +await stream.setSharePrivilege(SharePrivilege.Locked); // Only host can share +await stream.setSharePrivilege(SharePrivilege.OneParticipant); // One at a time + +// Lock/unlock share (host only) +await stream.lockShare(true); // Only host can share +await stream.lockShare(false); // Anyone can share +``` + +## Multiple Share Views (SDK 2.2.10+) + +```javascript +// Check max renderable share views +const maxViews = stream.getMaxRenderableShareViews(); +console.log('Can render up to', maxViews, 'share views'); + +// Get list of users who are sharing +const sharers = stream.getShareUserList(); + +// Render multiple share views +for (const sharer of sharers) { + const element = await stream.attachShareView(sharer.userId); + document.getElementById('multi-share-container').appendChild(element); +} +``` + +## Share Quality Optimization + +```javascript +// Optimize for video content (movies, animations) +await stream.enableOptimizeForSharedVideo(true); + +// Check if optimized +const isOptimized = stream.isOptimizeForSharedVideoEnabled(); + +// Check if optimization is supported +const isSupported = stream.isSupportOptimizedForSharedVideo(); + +// Update shared video quality +await stream.updateSharedVideoQuality(VideoQuality.Video_720P); +``` + +## Screenshot Share View + +```javascript +// Take screenshot of active share +const blob = await stream.screenshotShareView(); + +// Convert to image +const url = URL.createObjectURL(blob); +const img = document.createElement('img'); +img.src = url; +document.body.appendChild(img); +``` + +## React Component + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import { VideoClient, Stream } from '@zoom/videosdk'; + +interface ScreenShareProps { + client: typeof VideoClient; + stream: typeof Stream; +} + +export const ScreenShare: React.FC = ({ client, stream }) => { + const containerRef = useRef(null); + const [isSharing, setIsSharing] = useState(false); + const [activeShareUserId, setActiveShareUserId] = useState(null); + + useEffect(() => { + const handleShareChange = async (payload: { state: string; userId: number }) => { + if (payload.state === 'Active') { + setActiveShareUserId(payload.userId); + + if (containerRef.current) { + const element = await stream.attachShareView(payload.userId); + containerRef.current.innerHTML = ''; + containerRef.current.appendChild(element); + } + } else { + if (activeShareUserId) { + await stream.detachShareView(activeShareUserId); + } + setActiveShareUserId(null); + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + } + }; + + const handlePassiveStop = () => { + setIsSharing(false); + }; + + client.on('active-share-change', handleShareChange); + client.on('passively-stop-share', handlePassiveStop); + + return () => { + client.off('active-share-change', handleShareChange); + client.off('passively-stop-share', handlePassiveStop); + }; + }, [client, stream, activeShareUserId]); + + const startShare = async () => { + try { + const canvas = document.createElement('canvas'); + await stream.startShareScreen(canvas); + setIsSharing(true); + } catch (error) { + console.error('Share failed:', error); + } + }; + + const stopShare = async () => { + await stream.stopShareScreen(); + setIsSharing(false); + }; + + return ( +
+
+ {isSharing ? ( + + ) : ( + + )} +
+ + {activeShareUserId && ( +
+ )} +
+ ); +}; +``` + +## Key Events + +| Event | When | Payload | +|-------|------|---------| +| `active-share-change` | Someone starts/stops sharing | `{ state: 'Active' \| 'Inactive', userId }` | +| `share-content-change` | Sharer switches window/tab | `{ userId }` | +| `share-content-dimension-change` | Share resolution changes | `{ width, height, type }` | +| `passively-stop-share` | You're forced to stop | `PassiveStopShareReason` enum | +| `share-privilege-change` | Host changes share settings | `{ privilege }` | +| `peer-share-state-change` | Peer share state changes | `{ action: 'Start' \| 'Stop', userId }` | +| `share-audio-change` | Share audio state changes | `{ state: 'on' \| 'off' }` | + +## Key Points + +1. **Two methods for receiving**: Canvas-based (`startShareView`) or VideoPlayer-based (`attachShareView`) +2. **Check privileges first**: Use `getSharePrivilege()` and `isShareLocked()` before attempting to share +3. **Handle `passively-stop-share`**: You may be forced to stop sharing by the host +4. **Share audio requires user gesture**: Browsers require user interaction to share audio +5. **Different from video**: Screen share uses separate canvas/container from video + +## Related Documentation + +- [Video Rendering](video-rendering.md) - Video handling +- [Event Handling](event-handling.md) - All events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/session-join-pattern.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/session-join-pattern.md new file mode 100644 index 00000000..13f8aaf4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/session-join-pattern.md @@ -0,0 +1,389 @@ +# Session Join Pattern + +Complete working example for joining a Zoom Video SDK session. + +## Prerequisites + +1. Video SDK credentials from [Marketplace](https://marketplace.zoom.us/) +2. JWT token generated server-side +3. Modern browser (Chrome 80+, Firefox 75+, Safari 14+, Edge 80+) + +## Complete Example + +```javascript +import ZoomVideo, { VideoQuality } from '@zoom/videosdk'; + +// State +let client = null; +let stream = null; + +// Configuration +const config = { + language: 'en-US', + dependentAssets: 'Global', // or 'CDN', 'CN', or custom path + options: { + patchJsMedia: true, + // webrtc: true, // Enable for HD video + } +}; + +/** + * Initialize the SDK (call once) + */ +async function initializeSDK() { + // Step 1: Create client (singleton) + client = ZoomVideo.createClient(); + + // Optional: Check compatibility first + const compatibility = ZoomVideo.checkSystemRequirements(); + if (!compatibility.video || !compatibility.audio) { + throw new Error('Browser not compatible'); + } + + // Step 2: Initialize + await client.init(config.language, config.dependentAssets, config.options); + + console.log('SDK initialized successfully'); + return client; +} + +/** + * Join a session + * @param {string} topic - Session name (must match JWT tpc) + * @param {string} signature - Video SDK JWT from server + * @param {string} userName - Display name + * @param {string} password - Optional session password + */ +async function joinSession(topic, signature, userName, password = '') { + try { + // Step 3: Join session + await client.join(topic, signature, userName, password); + + // Step 4: Get stream (ONLY AFTER JOIN!) + stream = client.getMediaStream(); + + // Step 5: Set up event listeners + setupEventListeners(); + + // Step 6: Get current user info + const currentUser = client.getCurrentUserInfo(); + console.log('Joined as:', currentUser.displayName, 'ID:', currentUser.userId); + + return { client, stream, currentUser }; + } catch (error) { + handleJoinError(error); + throw error; + } +} + +/** + * Set up all required event listeners + */ +function setupEventListeners() { + // Connection state changes + client.on('connection-change', (payload) => { + console.log('Connection state:', payload.state); + + if (payload.state === 'Closed') { + console.log('Disconnected. Reason:', payload.reason); + cleanup(); + } + + if (payload.state === 'Reconnecting') { + console.log('Reconnecting...', payload.reason); + } + }); + + // New participant joined + client.on('user-added', (payload) => { + console.log('User(s) joined:', payload.map(u => u.displayName)); + // Create UI containers for new users + }); + + // Participant left + client.on('user-removed', (payload) => { + console.log('User(s) left:', payload.map(u => u.displayName)); + // Clean up UI for users + payload.forEach(user => { + stream.detachVideo(user.userId); + }); + }); + + // Participant updated (name, mute, etc.) + client.on('user-updated', (payload) => { + console.log('User(s) updated:', payload); + // Update UI state + }); + + // Peer video state change (CRITICAL for rendering) + client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + console.log(`User ${userId} video: ${action}`); + + if (action === 'Start') { + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + document.getElementById(`video-${userId}`)?.appendChild(element); + } else { + await stream.detachVideo(userId); + } + }); + + // Audio changes + client.on('current-audio-change', (payload) => { + console.log('Audio change:', payload.action, payload.type); + }); + + // Active speaker + client.on('active-speaker', (payload) => { + if (payload.length > 0) { + console.log('Active speaker:', payload[0].userId); + } + }); +} + +/** + * Start media after joining + */ +async function startMedia() { + // Start video + try { + await stream.startVideo(); + console.log('Video started'); + + // Attach own video + const currentUser = client.getCurrentUserInfo(); + const myVideoElement = await stream.attachVideo( + currentUser.userId, + VideoQuality.Video_360P + ); + document.getElementById('my-video')?.appendChild(myVideoElement); + } catch (error) { + console.error('Failed to start video:', error); + } + + // Start audio + try { + await stream.startAudio(); + console.log('Audio started'); + } catch (error) { + console.error('Failed to start audio:', error); + } + + // Render existing participants (for mid-session join) + await renderExistingParticipants(); +} + +/** + * Render videos of participants who joined before us + */ +async function renderExistingParticipants() { + // Small delay to ensure all participants are loaded + await new Promise(resolve => setTimeout(resolve, 500)); + + const users = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const user of users) { + if (user.bVideoOn && user.userId !== currentUserId) { + const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P); + document.getElementById(`video-${user.userId}`)?.appendChild(element); + } + } +} + +/** + * Handle join errors + */ +function handleJoinError(error) { + console.error('Join error:', error); + + const errorMessage = error.reason || error.message || 'Unknown error'; + + if (errorMessage.includes('signature')) { + console.error('Invalid signature - regenerate JWT'); + } else if (errorMessage.includes('Session')) { + console.error('Session not found - host may not have started'); + } else if (errorMessage.includes('password')) { + console.error('Invalid password'); + } else if (errorMessage.includes('Permission')) { + console.error('Permission denied - check camera/mic access'); + } +} + +/** + * Leave the session + * @param {boolean} end - If true, ends session for all (host only) + */ +async function leaveSession(end = false) { + try { + await client.leave(end); + console.log(end ? 'Session ended' : 'Left session'); + cleanup(); + } catch (error) { + console.error('Leave error:', error); + } +} + +/** + * Clean up resources + */ +function cleanup() { + stream = null; + // Remove event listeners, clear UI, etc. +} + +// Usage +async function main() { + await initializeSDK(); + + const { currentUser } = await joinSession( + 'my-session-topic', + 'YOUR_JWT_TOKEN', + 'User Name' + ); + + await startMedia(); + + console.log('Ready! User:', currentUser.displayName); +} + +main().catch(console.error); +``` + +## CDN Version + +```html + + + + Zoom Video SDK + + + + +
+
+ + + + +``` + +## React Example + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import ZoomVideo, { VideoClient, Stream, VideoQuality } from '@zoom/videosdk'; + +interface Props { + topic: string; + signature: string; + userName: string; +} + +export const VideoSession: React.FC = ({ topic, signature, userName }) => { + const [client, setClient] = useState(null); + const [stream, setStream] = useState(null); + const [isJoined, setIsJoined] = useState(false); + const videoContainerRef = useRef(null); + + // Initialize SDK + useEffect(() => { + const init = async () => { + const zmClient = ZoomVideo.createClient(); + await zmClient.init('en-US', 'Global', { patchJsMedia: true }); + setClient(zmClient); + }; + init(); + + return () => { + ZoomVideo.destroyClient(); + }; + }, []); + + // Join session + useEffect(() => { + if (!client || isJoined) return; + + const join = async () => { + await client.join(topic, signature, userName); + const mediaStream = client.getMediaStream(); + setStream(mediaStream); + setIsJoined(true); + }; + join(); + }, [client, topic, signature, userName, isJoined]); + + // Set up event listeners + useEffect(() => { + if (!client || !stream) return; + + const handleVideoChange = async (payload: { action: string; userId: number }) => { + const { action, userId } = payload; + if (action === 'Start') { + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + videoContainerRef.current?.appendChild(element); + } else { + await stream.detachVideo(userId); + } + }; + + client.on('peer-video-state-change', handleVideoChange); + + return () => { + client.off('peer-video-state-change', handleVideoChange); + }; + }, [client, stream]); + + // Start own video + useEffect(() => { + if (!stream) return; + + const startVideo = async () => { + await stream.startVideo(); + const currentUser = client!.getCurrentUserInfo(); + const element = await stream.attachVideo(currentUser.userId, VideoQuality.Video_360P); + videoContainerRef.current?.appendChild(element); + }; + startVideo(); + }, [stream, client]); + + return ( +
+
+ +
+ ); +}; +``` + +## Key Points + +1. **Lifecycle order**: `createClient() → init() → join() → getMediaStream()` +2. **Stream timing**: ONLY call `getMediaStream()` after `join()` completes +3. **Event-driven**: Set up event listeners to handle participant video +4. **Mid-session join**: Manually render existing participants' videos +5. **Error handling**: Handle join errors gracefully + +## Related Documentation + +- [Video Rendering](video-rendering.md) - attachVideo patterns +- [Event Handling](event-handling.md) - Required events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/transcription.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/transcription.md new file mode 100644 index 00000000..08c1a6d3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/transcription.md @@ -0,0 +1,531 @@ +# Live Transcription + +Complete guide to implementing live transcription in the Zoom Video SDK for Web. + +## Prerequisites + +- Live transcription must be enabled on your Zoom account +- Session must have live transcription feature available + +## Getting the Transcription Client + +```javascript +// Get transcription client after joining session +const transcriptionClient = client.getLiveTranscriptionClient(); +``` + +## Basic Operations + +### Start Live Transcription + +```javascript +try { + await transcriptionClient.startLiveTranscription(); + console.log('Live transcription started'); +} catch (error) { + console.error('Failed to start transcription:', error); +} +``` + +### Check Transcription Status + +```javascript +const status = transcriptionClient.getLiveTranscriptionStatus(); + +console.log('Transcription enabled:', status.isLiveTranscriptionEnabled); +console.log('Caption language:', status.captionLanguage); +console.log('Translation settings:', status.translatedSetting); +``` + +### Set Speaking Language + +```javascript +import { LiveTranscriptionLanguage } from '@zoom/videosdk'; + +// Set the language you're speaking +await transcriptionClient.setSpeakingLanguage( + LiveTranscriptionLanguage.English +); +``` + +### Set Translation Language + +```javascript +// Translate transcriptions to another language +await transcriptionClient.setTranslationLanguage( + LiveTranscriptionLanguage.Spanish +); + +// Disable translation +await transcriptionClient.setTranslationLanguage(); // No argument +``` + +## Receiving Transcription Messages + +```javascript +// Listen for transcription text +client.on('caption-message', (payload) => { + const { + msgId, // Message ID + text, // Transcribed text + userId, // Speaker's user ID + displayName, // Speaker's display name + source, // 'caption' or 'translation' + language, // Language code + done, // true = final, false = interim + timestamp, // Unix timestamp + } = payload; + + if (done) { + // Final transcription - display permanently + addFinalTranscription(displayName, text); + } else { + // Interim result - update in place + updateInterimTranscription(displayName, text); + } +}); +``` + +## Transcription Events + +### Caption Status Changes + +```javascript +client.on('caption-status', (payload) => { + const { + autoCaption, // Auto-captioning enabled + language, // Caption language name + lang, // Language code + sessionLanguage, // Session's transcription language + translationStarted, // Translation active + } = payload; + + console.log('Caption status:', payload); +}); +``` + +### Captions Enabled/Disabled + +```javascript +client.on('caption-enable', (isEnabled) => { + if (isEnabled) { + showCaptionUI(); + } else { + hideCaptionUI(); + } +}); +``` + +### Caption Language Locked + +```javascript +client.on('caption-language-lock', (isLocked) => { + if (isLocked) { + // Host has locked the transcription language + disableLanguageSelector(); + } else { + enableLanguageSelector(); + } +}); +``` + +### Host Disabled Captions + +```javascript +client.on('caption-host-disable', (isDisabled) => { + if (isDisabled) { + console.log('Host has disabled captions'); + hideCaptionUI(); + } +}); +``` + +## Get Transcription History + +```javascript +// Get full transcription history +const history = transcriptionClient.getFullTranscriptionHistory(); + +// May return Promise for large histories (100,000+ records) +if (history instanceof Promise) { + const records = await history; + displayHistory(records); +} else { + displayHistory(history); +} + +// Get latest transcription +const latest = transcriptionClient.getLatestTranscription(); +console.log('Latest:', latest); + +// Get latest translation +const latestTranslation = transcriptionClient.getLatestTranslation(); +console.log('Latest translation:', latestTranslation); +``` + +## Get Current Languages + +```javascript +// Get current transcription language +const transcriptionLang = transcriptionClient.getCurrentTranscriptionLanguage(); +console.log('Transcription language:', transcriptionLang); + +// Get current translation language +const translationLang = transcriptionClient.getCurrentTranslationLanguage(); +console.log('Translation language:', translationLang); +``` + +## Host Controls + +### Lock Transcription Language + +```javascript +// Host can lock the transcription language +await transcriptionClient.lockTranscriptionLanguage(true); // Lock +await transcriptionClient.lockTranscriptionLanguage(false); // Unlock +``` + +### Disable Captions + +```javascript +// Host can disable captions for everyone +await transcriptionClient.disableCaptions(true); // Disable +await transcriptionClient.disableCaptions(false); // Enable +``` + +## Complete Transcription Manager + +```javascript +import { LiveTranscriptionLanguage } from '@zoom/videosdk'; + +class TranscriptionManager { + constructor(client) { + this.client = client; + this.transcriptionClient = null; + this.transcriptions = []; + this.interimMap = new Map(); // Track interim results by speaker + this.onTranscriptionUpdate = null; + this.onStatusChange = null; + } + + init() { + this.transcriptionClient = this.client.getLiveTranscriptionClient(); + this.setupEventListeners(); + } + + setupEventListeners() { + // Transcription messages + this.client.on('caption-message', (payload) => { + this.handleCaptionMessage(payload); + }); + + // Status changes + this.client.on('caption-status', (payload) => { + if (this.onStatusChange) { + this.onStatusChange(payload); + } + }); + + // Captions enabled/disabled + this.client.on('caption-enable', (isEnabled) => { + if (this.onStatusChange) { + this.onStatusChange({ enabled: isEnabled }); + } + }); + } + + handleCaptionMessage(payload) { + const { userId, displayName, text, done, source, timestamp } = payload; + const key = `${userId}-${source}`; + + if (done) { + // Final result - move from interim to final + this.interimMap.delete(key); + this.transcriptions.push({ + userId, + displayName, + text, + source, + timestamp, + isFinal: true, + }); + } else { + // Interim result - update in place + this.interimMap.set(key, { + userId, + displayName, + text, + source, + timestamp, + isFinal: false, + }); + } + + if (this.onTranscriptionUpdate) { + this.onTranscriptionUpdate(this.getAllTranscriptions()); + } + } + + getAllTranscriptions() { + // Combine final transcriptions with current interim results + const interim = Array.from(this.interimMap.values()); + return [...this.transcriptions, ...interim]; + } + + async start() { + await this.transcriptionClient.startLiveTranscription(); + } + + async setSpeakingLanguage(language) { + await this.transcriptionClient.setSpeakingLanguage(language); + } + + async setTranslationLanguage(language) { + await this.transcriptionClient.setTranslationLanguage(language); + } + + async disableTranslation() { + await this.transcriptionClient.setTranslationLanguage(); + } + + getStatus() { + return this.transcriptionClient.getLiveTranscriptionStatus(); + } + + async getHistory() { + const history = this.transcriptionClient.getFullTranscriptionHistory(); + if (history instanceof Promise) { + return await history; + } + return history; + } +} + +// Usage +const transcriptionManager = new TranscriptionManager(client); +transcriptionManager.init(); + +transcriptionManager.onTranscriptionUpdate = (transcriptions) => { + renderTranscriptions(transcriptions); +}; + +// Start transcription +document.getElementById('start-captions').onclick = async () => { + await transcriptionManager.start(); +}; + +// Change language +document.getElementById('language-select').onchange = async (e) => { + await transcriptionManager.setSpeakingLanguage(e.target.value); +}; +``` + +## React Component + +```typescript +import React, { useState, useEffect, useRef } from 'react'; +import { VideoClient, LiveTranscriptionLanguage } from '@zoom/videosdk'; + +interface TranscriptionEntry { + displayName: string; + text: string; + timestamp: number; + isFinal: boolean; + source: string; +} + +interface TranscriptionProps { + client: typeof VideoClient; + isHost: boolean; +} + +export const LiveTranscription: React.FC = ({ + client, + isHost +}) => { + const [transcriptions, setTranscriptions] = useState([]); + const [interimMap] = useState(new Map()); + const [isEnabled, setIsEnabled] = useState(false); + const [speakingLanguage, setSpeakingLanguage] = useState(''); + const containerRef = useRef(null); + + const transcriptionClient = client.getLiveTranscriptionClient(); + + useEffect(() => { + // Get initial status + const status = transcriptionClient.getLiveTranscriptionStatus(); + setIsEnabled(status.isLiveTranscriptionEnabled); + + // Handle caption messages + const handleCaption = (payload: any) => { + const { userId, displayName, text, done, source, timestamp } = payload; + const key = `${userId}-${source}`; + + if (done) { + interimMap.delete(key); + setTranscriptions(prev => [...prev, { + displayName, + text, + timestamp, + isFinal: true, + source, + }]); + } else { + interimMap.set(key, { + displayName, + text, + timestamp, + isFinal: false, + source, + }); + // Force re-render + setTranscriptions(prev => [...prev]); + } + }; + + const handleEnable = (enabled: boolean) => { + setIsEnabled(enabled); + }; + + client.on('caption-message', handleCaption); + client.on('caption-enable', handleEnable); + + return () => { + client.off('caption-message', handleCaption); + client.off('caption-enable', handleEnable); + }; + }, [client, transcriptionClient, interimMap]); + + // Auto-scroll + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [transcriptions]); + + const startTranscription = async () => { + await transcriptionClient.startLiveTranscription(); + }; + + const handleLanguageChange = async (e: React.ChangeEvent) => { + const language = e.target.value as LiveTranscriptionLanguage; + setSpeakingLanguage(language); + await transcriptionClient.setSpeakingLanguage(language); + }; + + // Combine final and interim transcriptions for display + const allTranscriptions = [ + ...transcriptions, + ...Array.from(interimMap.values()), + ].sort((a, b) => a.timestamp - b.timestamp); + + return ( +
+
+

Live Transcription

+ + {!isEnabled && ( + + )} + + +
+ +
+ {allTranscriptions.map((entry, index) => ( +
+ {entry.displayName}: + {entry.text} +
+ ))} +
+ + +
+ ); +}; +``` + +## Available Languages + +```typescript +// LiveTranscriptionLanguage enum +LiveTranscriptionLanguage.English +LiveTranscriptionLanguage.Spanish +LiveTranscriptionLanguage.French +LiveTranscriptionLanguage.German +LiveTranscriptionLanguage.Italian +LiveTranscriptionLanguage.Portuguese +LiveTranscriptionLanguage.Russian +LiveTranscriptionLanguage.Chinese +LiveTranscriptionLanguage.Japanese +LiveTranscriptionLanguage.Korean +// ... and more +``` + +## Key Points + +1. **Interim vs Final** - `done=false` is interim (updating), `done=true` is final +2. **Start transcription** - Call `startLiveTranscription()` to enable +3. **Set speaking language** - Tell the system what language you're speaking +4. **Translation is optional** - Use `setTranslationLanguage()` if needed +5. **Handle large histories** - `getFullTranscriptionHistory()` may return Promise + +## Key Events + +| Event | When | Payload | +|-------|------|---------| +| `caption-message` | Transcription text received | Text, speaker, done flag | +| `caption-status` | Status changes | Language, enabled state | +| `caption-enable` | Captions enabled/disabled | Boolean | +| `caption-language-lock` | Language locked by host | Boolean | +| `caption-host-disable` | Host disabled captions | Boolean | + +## Related Documentation + +- [Event Handling](event-handling.md) - Transcription events +- [API Reference](../references/web-reference.md) - Full LiveTranscriptionClient API diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/examples/video-rendering.md b/partner-built/zoom-plugin/skills/video-sdk/web/examples/video-rendering.md new file mode 100644 index 00000000..9c972306 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/examples/video-rendering.md @@ -0,0 +1,384 @@ +# Video Rendering + +Complete guide to rendering video using `attachVideo()` in the Zoom Video SDK for Web. + +## Critical Rule + +**NEVER use `renderVideo()` - it's deprecated. Always use `attachVideo()`.** + +## VideoQuality Enum + +```typescript +import { VideoQuality } from '@zoom/videosdk'; + +// Available quality levels (value = numeric enum) +VideoQuality.Video_90P // 0 - Thumbnail +VideoQuality.Video_180P // 1 - Low quality +VideoQuality.Video_360P // 2 - Standard (recommended default) +VideoQuality.Video_720P // 3 - HD +VideoQuality.Video_1080P // 4 - Full HD (requires webrtc mode) +``` + +## Basic Video Rendering + +### Start and Render Own Video + +```javascript +import ZoomVideo, { VideoQuality } from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +let stream; + +async function startOwnVideo() { + // Ensure you're joined first + stream = client.getMediaStream(); + + // Step 1: Start capturing video + await stream.startVideo(); + + // Step 2: Get current user ID + const currentUser = client.getCurrentUserInfo(); + + // Step 3: Attach video to DOM + const videoElement = await stream.attachVideo( + currentUser.userId, + VideoQuality.Video_360P + ); + + // Step 4: Add to container + document.getElementById('my-video-container').appendChild(videoElement); +} +``` + +### Render Remote Participant Video + +```javascript +// Listen for remote video state changes +client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + + if (action === 'Start') { + // Remote user started video - render it + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + + // Add to their video container + const container = document.getElementById(`video-${userId}`); + if (container) { + container.appendChild(element); + } + } else if (action === 'Stop') { + // Remote user stopped video - remove element + await stream.detachVideo(userId); + + // Clean up DOM + const container = document.getElementById(`video-${userId}`); + if (container) { + container.innerHTML = ''; + } + } +}); +``` + +## Mid-Session Join: Rendering Existing Participants + +When you join a session that already has participants with video on, you won't receive `peer-video-state-change` events for them. You must manually render their videos. + +```javascript +async function renderExistingParticipants() { + // Wait a moment for participant list to populate + await new Promise(resolve => setTimeout(resolve, 500)); + + const participants = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const participant of participants) { + // Skip self + if (participant.userId === currentUserId) continue; + + // Check if they have video on + if (participant.bVideoOn) { + const element = await stream.attachVideo( + participant.userId, + VideoQuality.Video_360P + ); + + const container = document.getElementById(`video-${participant.userId}`); + if (container) { + container.appendChild(element); + } + } + } +} + +// Call after joining and starting your own video +await startOwnVideo(); +await renderExistingParticipants(); +``` + +## Quality Selection Strategy + +```javascript +// Determine quality based on use case +function getQualityForLayout(totalParticipants, isSpotlight = false) { + if (isSpotlight) { + // Spotlighted/active speaker - highest quality + return VideoQuality.Video_720P; + } + + if (totalParticipants <= 4) { + // Small meeting - good quality for all + return VideoQuality.Video_360P; + } + + if (totalParticipants <= 9) { + // Medium meeting - balanced quality + return VideoQuality.Video_180P; + } + + // Large meeting - thumbnails + return VideoQuality.Video_90P; +} + +// Dynamic quality adjustment +async function updateVideoQualities() { + const participants = client.getAllUser().filter(p => p.bVideoOn); + const quality = getQualityForLayout(participants.length); + + for (const participant of participants) { + // Re-attach with new quality + await stream.detachVideo(participant.userId); + const element = await stream.attachVideo(participant.userId, quality); + + const container = document.getElementById(`video-${participant.userId}`); + if (container) { + container.innerHTML = ''; + container.appendChild(element); + } + } +} +``` + +## HD Video (720P/1080P) + +To use HD video, enable WebRTC mode during initialization: + +```javascript +await client.init('en-US', 'Global', { + patchJsMedia: true, + webrtc: true, // Required for HD video +}); + +// Now you can use higher qualities +const element = await stream.attachVideo(userId, VideoQuality.Video_720P); + +// Check if HD is supported on this device +if (stream.isSupportHDVideo()) { + const hdElement = await stream.attachVideo(userId, VideoQuality.Video_1080P); +} +``` + +## Multiple Video Rendering + +Check device capability for rendering multiple videos: + +```javascript +// Check max renderable videos +const maxVideos = stream.getMaxRenderableVideos(); +console.log('Can render up to', maxVideos, 'videos'); + +// Check if multiple video rendering is supported +if (stream.isSupportMultipleVideos()) { + // Can render multiple participant videos +} else { + // Limited to fewer simultaneous videos + // Consider using active speaker mode +} +``` + +## Detaching Video + +```javascript +// Detach specific user's video +const elements = await stream.detachVideo(userId); + +// elements can be a single element or array +if (Array.isArray(elements)) { + elements.forEach(el => el.remove()); +} else { + elements.remove(); +} + +// Detach from specific element +const specificElement = document.querySelector(`#video-${userId} video-player`); +await stream.detachVideo(userId, specificElement); +``` + +## Mirror Self Video + +```javascript +// Mirror your own video (selfie mode) +await stream.mirrorVideo(true); + +// Check current mirror state +const isMirrored = stream.isVideoMirrored(); +``` + +## Stop Video + +```javascript +// Stop capturing video (turns off camera) +await stream.stopVideo(); + +// The attached video element will show black/placeholder +// Detach to clean up +const currentUser = client.getCurrentUserInfo(); +await stream.detachVideo(currentUser.userId); +``` + +## Complete React Component + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import ZoomVideo, { VideoClient, Stream, VideoQuality } from '@zoom/videosdk'; + +interface VideoTileProps { + userId: number; + stream: typeof Stream; + quality?: VideoQuality; +} + +export const VideoTile: React.FC = ({ + userId, + stream, + quality = VideoQuality.Video_360P +}) => { + const containerRef = useRef(null); + const [isAttached, setIsAttached] = useState(false); + + useEffect(() => { + let mounted = true; + + const attachVideo = async () => { + if (!containerRef.current || !stream) return; + + try { + const element = await stream.attachVideo(userId, quality); + if (mounted && containerRef.current) { + containerRef.current.innerHTML = ''; + containerRef.current.appendChild(element); + setIsAttached(true); + } + } catch (error) { + console.error('Failed to attach video:', error); + } + }; + + attachVideo(); + + return () => { + mounted = false; + if (isAttached) { + stream.detachVideo(userId).catch(console.error); + } + }; + }, [userId, stream, quality, isAttached]); + + return ( +
+ ); +}; + +// Usage in parent component +export const VideoGrid: React.FC<{ client: typeof VideoClient; stream: typeof Stream }> = ({ + client, + stream +}) => { + const [participants, setParticipants] = useState([]); + + useEffect(() => { + // Initial load + setParticipants(client.getAllUser().filter(p => p.bVideoOn)); + + // Listen for changes + const handleVideoChange = async (payload: { action: string; userId: number }) => { + if (payload.action === 'Start') { + setParticipants(prev => { + const user = client.getUser(payload.userId); + if (user && !prev.find(p => p.userId === payload.userId)) { + return [...prev, user]; + } + return prev; + }); + } else { + setParticipants(prev => prev.filter(p => p.userId !== payload.userId)); + } + }; + + client.on('peer-video-state-change', handleVideoChange); + + return () => { + client.off('peer-video-state-change', handleVideoChange); + }; + }, [client]); + + const quality = participants.length <= 4 + ? VideoQuality.Video_360P + : VideoQuality.Video_180P; + + return ( +
+ {participants.map(p => ( + + ))} +
+ ); +}; +``` + +## Error Handling + +```javascript +async function safeAttachVideo(userId, quality) { + try { + const element = await stream.attachVideo(userId, quality); + return element; + } catch (error) { + console.error('attachVideo failed:', error); + + if (error.type === 'INVALID_OPERATION') { + // User may have stopped video, or not in session + console.log('User video not available'); + } else if (error.type === 'INTERNAL_ERROR') { + // SDK internal error - may need to retry + await new Promise(r => setTimeout(r, 1000)); + return stream.attachVideo(userId, quality); + } + + return null; + } +} +``` + +## Key Points + +1. **Use `attachVideo()`, NOT `renderVideo()`** - `renderVideo()` is deprecated +2. **Listen to `peer-video-state-change`** - Required for remote video +3. **Handle mid-session join** - Manually render existing participants +4. **Detach before re-attaching** - When changing quality +5. **Check device capabilities** - `getMaxRenderableVideos()`, `isSupportMultipleVideos()` +6. **Enable WebRTC for HD** - Required for 720P/1080P + +## Related Documentation + +- [Session Join](session-join-pattern.md) - Initial setup +- [Event Handling](event-handling.md) - All video events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/references/events-reference.md b/partner-built/zoom-plugin/skills/video-sdk/web/references/events-reference.md new file mode 100644 index 00000000..1b91722a --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/references/events-reference.md @@ -0,0 +1,696 @@ +# Events Reference + +Complete reference for all events in the Zoom Video SDK for Web. + +## Event Registration + +```javascript +// Register +client.on('event-name', (payload) => { /* handle */ }); + +// Unregister +client.off('event-name', handler); +``` + +--- + +## Session Events + +### connection-change + +Fired when connection state changes. + +```typescript +client.on('connection-change', (payload: { + state: 'Connected' | 'Connecting' | 'Reconnecting' | 'Closed' | 'Fail'; + reason?: string; +}) => {}); +``` + +| State | Description | +|-------|-------------| +| `Connected` | Successfully connected to session | +| `Connecting` | Attempting to connect | +| `Reconnecting` | Lost connection, attempting reconnect | +| `Closed` | Disconnected from session | +| `Fail` | Connection failed | + +--- + +## Participant Events + +### user-added + +Fired when participant(s) join the session. + +```typescript +client.on('user-added', (payload: Participant[]) => {}); + +interface Participant { + userId: number; + displayName: string; + avatar?: string; + isHost: boolean; + isManager: boolean; + bVideoOn: boolean; + muted: boolean; + // ... more properties +} +``` + +### user-removed + +Fired when participant(s) leave the session. + +```typescript +client.on('user-removed', (payload: Participant[]) => {}); +``` + +### user-updated + +Fired when participant properties change (name, mute, video, etc.). + +```typescript +client.on('user-updated', (payload: Participant[]) => {}); +``` + +--- + +## Video Events + +### peer-video-state-change + +Fired when a remote participant starts or stops video. + +```typescript +client.on('peer-video-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +**Critical**: Use this to render/remove remote videos. + +### video-capturing-change + +Fired when local video capture state changes. + +```typescript +client.on('video-capturing-change', (payload: { + state: 'Started' | 'Stopped' | 'Failed'; +}) => {}); +``` + +### video-active-change + +Fired when video becomes active/inactive. + +```typescript +client.on('video-active-change', (payload: { + state: VideoActiveState; + userId: number; +}) => {}); +``` + +### video-dimension-change + +Fired when received video dimensions change. + +```typescript +client.on('video-dimension-change', (payload: { + width: number; + height: number; + type: 'received'; +}) => {}); +``` + +### video-detailed-data-change + +Fired when video quality details change. + +```typescript +client.on('video-detailed-data-change', (payload: { + userId: number; + width?: number; + height?: number; + fps?: number; + quality?: VideoQuality; +}) => {}); +``` + +### video-spotlight-change + +Fired when spotlight list changes. + +```typescript +client.on('video-spotlight-change', (payload: { + spotlightList: { userId: number }[]; +}) => {}); +``` + +--- + +## Audio Events + +### current-audio-change + +Fired when local audio state changes. + +```typescript +client.on('current-audio-change', (payload: { + action: 'join' | 'leave' | 'muted' | 'unmuted'; + type?: 'computer' | 'phone'; + source?: MutedSource | LeaveAudioSource; +}) => {}); +``` + +| Action | Description | +|--------|-------------| +| `join` | Joined audio (computer or phone) | +| `leave` | Left audio | +| `muted` | Audio muted | +| `unmuted` | Audio unmuted | + +| Source | Description | +|--------|-------------| +| `active` | User action | +| `passive(mute all)` | Host muted all | +| `passive(mute one)` | Host muted you | +| `passive` | Host unmuted you | + +### host-ask-unmute-audio + +Fired when host asks you to unmute. + +```typescript +client.on('host-ask-unmute-audio', (payload: { + reason: 'Unmute' | 'Spotlight' | 'Allow to talk'; +}) => {}); +``` + +### active-speaker + +Fired when active speakers change. + +```typescript +client.on('active-speaker', (payload: { + userId: number; + displayName: string; +}[]) => {}); +``` + +Array is sorted by volume (loudest first). + +### auto-play-audio-failed + +Fired when browser blocks audio autoplay. + +```typescript +client.on('auto-play-audio-failed', () => {}); +``` + +### current-audio-level-change + +Fired when local audio level changes. + +```typescript +client.on('current-audio-level-change', (payload: { + level: number; +}) => {}); +``` + +### speaking-while-muted + +Fired when speaking while muted is detected. + +```typescript +client.on('speaking-while-muted', () => {}); +``` + +--- + +## Screen Share Events + +### active-share-change + +Fired when someone starts/stops sharing. + +```typescript +client.on('active-share-change', (payload: { + state: 'Active' | 'Inactive'; + userId: number; +}) => {}); +``` + +### peer-share-state-change + +Fired when peer share state changes. + +```typescript +client.on('peer-share-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +### passively-stop-share + +Fired when you're forced to stop sharing. + +```typescript +client.on('passively-stop-share', (payload: PassiveStopShareReason) => {}); + +// Reasons: 'PrivilegeChange', 'AnotherShareStarted', etc. +``` + +### share-content-change + +Fired when sharer switches windows/tabs. + +```typescript +client.on('share-content-change', (payload: { + userId: number; +}) => {}); +``` + +### share-content-dimension-change + +Fired when share dimensions change. + +```typescript +client.on('share-content-dimension-change', (payload: { + width: number; + height: number; + type: 'received' | 'sended'; +}) => {}); +``` + +### share-privilege-change + +Fired when share privilege changes. + +```typescript +client.on('share-privilege-change', (payload: { + privilege: SharePrivilege; +}) => {}); +``` + +### share-audio-change + +Fired when share audio state changes. + +```typescript +client.on('share-audio-change', (payload: { + state: 'on' | 'off'; +}) => {}); +``` + +--- + +## Chat Events + +### chat-on-message + +Fired when a chat message is received. + +```typescript +client.on('chat-on-message', (payload: { + id: string; + message: string; + sender: { userId: number; name: string; avatar?: string }; + receiver: { userId: number; name: string } | 'everyone'; + timestamp: number; + isPrivate: boolean; +}) => {}); +``` + +### chat-privilege-change + +Fired when chat privilege changes. + +```typescript +client.on('chat-privilege-change', (payload: { + chatPrivilege: ChatPrivilege; +}) => {}); +``` + +### chat-file-upload-progress + +Fired during file upload. + +```typescript +client.on('chat-file-upload-progress', (payload: { + fileName: string; + fileSize: number; + progress: number; + status: ChatFileUploadStatus; + receiverId: number; + retryToken?: string; +}) => {}); +``` + +### chat-file-download-progress + +Fired during file download. + +```typescript +client.on('chat-file-download-progress', (payload: { + fileName: string; + fileSize: number; + progress: number; + status: ChatFileDownloadStatus; + fileUrl: string; + fileBlob?: Blob; + senderId: number; +}) => {}); +``` + +--- + +## Recording Events + +### recording-change + +Fired when cloud recording state changes. + +```typescript +client.on('recording-change', (payload: { + state: RecordingStatus; +}) => {}); +``` + +| Status | Description | +|--------|-------------| +| `Recording` | Recording in progress | +| `Paused` | Recording paused | +| `Stopped` | Recording stopped | + +### individual-recording-change + +Fired for individual recording consent. + +```typescript +client.on('individual-recording-change', (payload: { + state: RecordingStatus | 'Ask' | 'Accept' | 'Decline'; + userId?: number; +}) => {}); +``` + +--- + +## Live Transcription Events + +### caption-message + +Fired when transcription text is received. + +```typescript +client.on('caption-message', (payload: { + msgId: string; + text: string; + userId: number; + displayName: string; + source: 'caption' | 'translation'; + language: string; + done: boolean; // true = final, false = interim + timestamp: number; +}) => {}); +``` + +### caption-status + +Fired when caption status changes. + +```typescript +client.on('caption-status', (payload: { + autoCaption: boolean; + language?: string; + lang?: number; + sessionLanguage?: string; + translationStarted?: boolean; +}) => {}); +``` + +### caption-enable + +Fired when captions are enabled/disabled. + +```typescript +client.on('caption-enable', (isEnabled: boolean) => {}); +``` + +### caption-language-lock + +Fired when language is locked/unlocked. + +```typescript +client.on('caption-language-lock', (isLocked: boolean) => {}); +``` + +### caption-host-disable + +Fired when host disables captions. + +```typescript +client.on('caption-host-disable', (isDisabled: boolean) => {}); +``` + +--- + +## Device Events + +### device-change + +Fired when devices are added/removed. + +```typescript +client.on('device-change', () => {}); +``` + +### device-permission-change + +Fired when device permissions change. + +```typescript +client.on('device-permission-change', (payload: { + name: 'microphone' | 'camera'; + state: 'granted' | 'denied' | 'prompt'; +}) => {}); +``` + +--- + +## Network & Quality Events + +### network-quality-change + +Fired when network quality changes. + +```typescript +client.on('network-quality-change', (payload: { + userId: number; + type: 'uplink' | 'downlink'; + level: number; // 0-5 (0=unknown, 1=bad, 5=excellent) +}) => {}); +``` + +### video-statistic-data-change + +Fired when video statistics change. + +```typescript +client.on('video-statistic-data-change', (payload: { + type: 'VIDEO_QOS_DATA'; + data: { + encoding: boolean; // true=send, false=receive + width: number; + height: number; + fps: number; + bitrate: number; + avg_loss: number; + max_loss: number; + jitter: number; + rtt: number; + }; +}) => {}); +``` + +### audio-statistic-data-change + +Fired when audio statistics change. + +```typescript +client.on('audio-statistic-data-change', (payload: { + type: 'AUDIO_QOS_DATA'; + data: { + encoding: boolean; + bitrate: number; + avg_loss: number; + max_loss: number; + jitter: number; + rtt: number; + sample_rate: number; + }; +}) => {}); +``` + +--- + +## Command Channel Events + +### command-channel-status + +Fired when command channel status changes. + +```typescript +client.on('command-channel-status', (payload: ConnectionState) => {}); +``` + +### command-channel-message + +Fired when command channel message is received. + +```typescript +client.on('command-channel-message', (payload: { + msgid: string; + senderId: string; + senderName: string; + text: string; + timestamp: number; +}) => {}); +``` + +--- + +## Live Stream Events + +### live-stream-status + +Fired when live stream status changes. + +```typescript +client.on('live-stream-status', (status: LiveStreamStatus) => {}); +``` + +--- + +## Far End Camera Control Events + +### far-end-camera-request-control + +Fired when someone requests camera control. + +```typescript +client.on('far-end-camera-request-control', (payload: { + userId: number; + displayName: string; + currentControllingUserId?: number; + currentControllingDisplayName?: string; +}) => {}); +``` + +### far-end-camera-response-control + +Fired with camera control response. + +```typescript +client.on('far-end-camera-response-control', (payload: { + userId: number; + displayName: string; + isApproved: boolean; + reason?: FarEndCameraControlDeclinedReason; +}) => {}); +``` + +### far-end-camera-capability-change + +Fired when camera capabilities change. + +```typescript +client.on('far-end-camera-capability-change', (payload: { + userId: number; + ptz: PTZCameraCapability; +}) => {}); +``` + +--- + +## Subsession Events + +### subsession-invite-to-join + +Fired when invited to join subsession. + +```typescript +client.on('subsession-invite-to-join', (payload: { + subsessionId: string; + subsessionName: string; +}) => {}); +``` + +### subsession-broadcast-message + +Fired when broadcast message received. + +```typescript +client.on('subsession-broadcast-message', (payload: { + message: string; +}) => {}); +``` + +### subsession-state-change + +Fired when subsession state changes. + +```typescript +client.on('subsession-state-change', (payload: { + status: SubsessionStatus; +}) => {}); +``` + +--- + +## Whiteboard Events + +### whiteboard-status-change + +Fired when whiteboard status changes. + +```typescript +client.on('whiteboard-status-change', (status: WhiteboardStatus) => {}); +``` + +### peer-whiteboard-state-change + +Fired when peer whiteboard state changes. + +```typescript +client.on('peer-whiteboard-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +--- + +## Event Categories Quick Reference + +| Category | Events | +|----------|--------| +| **Session** | `connection-change` | +| **Participants** | `user-added`, `user-removed`, `user-updated` | +| **Video** | `peer-video-state-change`, `video-capturing-change`, `video-dimension-change`, `video-active-change` | +| **Audio** | `current-audio-change`, `host-ask-unmute-audio`, `active-speaker`, `auto-play-audio-failed` | +| **Share** | `active-share-change`, `passively-stop-share`, `share-privilege-change`, `share-content-change` | +| **Chat** | `chat-on-message`, `chat-privilege-change`, `chat-file-*-progress` | +| **Recording** | `recording-change`, `individual-recording-change` | +| **Transcription** | `caption-message`, `caption-status`, `caption-enable` | +| **Device** | `device-change`, `device-permission-change` | +| **Network** | `network-quality-change`, `*-statistic-data-change` | + +--- + +## Related Documentation + +- [Event Handling Examples](../examples/event-handling.md) +- [API Reference](web-reference.md) diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/references/web-reference.md b/partner-built/zoom-plugin/skills/video-sdk/web/references/web-reference.md new file mode 100644 index 00000000..e1c27a3d --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/references/web-reference.md @@ -0,0 +1,411 @@ +# Zoom Video SDK Web - API Reference + +## Overview + +This reference provides the complete API for the Zoom Video SDK for Web. The SDK follows a hierarchical pattern: + +``` +ZoomVideo (module) + └── VideoClient (singleton) + ├── Stream (media operations) + └── Feature Clients (chat, recording, etc.) +``` + +**Official API Reference**: https://marketplacefront.zoom.us/sdk/custom/web/modules.html + +--- + +## Level 0: ZoomVideo Module + +### Static Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `createClient()` | `VideoClient` | Creates/returns the singleton client | +| `destroyClient()` | `Promise` | Destroys the client instance | +| `checkSystemRequirements()` | `MediaCompatibility` | Check browser compatibility | +| `checkFeatureRequirements()` | `SupportFeatures` | Check feature support | +| `getDevices(skip?)` | `Promise` | Enumerate media devices | +| `preloadDependentAssets(path?)` | `void` | Preload SDK assets | +| `createLocalVideoTrack(id?)` | `LocalVideoTrack` | Create local video track for preview | +| `createLocalAudioTrack(id?)` | `LocalAudioTrack` | Create local audio track for preview | +| `VERSION` | `string` | SDK version | + +### MediaCompatibility Interface + +```typescript +interface MediaCompatibility { + audio: boolean; // Audio support + video: boolean; // Video support + screen: boolean; // Screen share support +} +``` + +### SupportFeatures Interface + +```typescript +interface SupportFeatures { + platform: string; // Browser/platform info + supportFeatures: string[]; // Supported features + unSupportFeatures: string[]; // Unsupported features +} +``` + +--- + +## Level 1: VideoClient + +### Session Lifecycle + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `init` | `(language, dependentAssets, options?)` | `ExecutedResult` | Initialize SDK | +| `join` | `(topic, token, userName, password?, timeout?)` | `ExecutedResult` | Join session | +| `leave` | `(end?)` | `ExecutedResult` | Leave/end session | +| `on` | `(event, callback)` | `void` | Subscribe to events | +| `off` | `(event, callback)` | `void` | Unsubscribe from events | + +### InitOptions Interface + +```typescript +interface InitOptions { + patchJsMedia?: boolean; // Patch JS media (recommended: true) + webrtc?: boolean; // Enable WebRTC mode for HD + enforceMultipleVideos?: boolean; // Force multi-video mode + stayAwake?: boolean; // Prevent screen sleep +} +``` + +### Participant Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getAllUser()` | `Participant[]` | Get all participants | +| `getCurrentUserInfo()` | `Participant` | Get current user | +| `getUser(userId)` | `Participant \| undefined` | Get user by ID | +| `getSessionHost()` | `Participant \| undefined` | Get session host | +| `getSessionInfo()` | `SessionInfo` | Get session info | +| `isHost()` | `boolean` | Is current user host | +| `isManager()` | `boolean` | Is current user manager | +| `isOriginalHost()` | `boolean` | Is current user original host | + +### Host Controls + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `makeHost` | `(userId)` | `ExecutedResult` | Make user host | +| `makeManager` | `(userId)` | `ExecutedResult` | Make user manager | +| `revokeManager` | `(userId)` | `ExecutedResult` | Remove manager | +| `removeUser` | `(userId)` | `ExecutedResult` | Remove user from session | +| `changeName` | `(name, userId?)` | `ExecutedResult` | Change display name | +| `reclaimHost` | `()` | `ExecutedResult` | Reclaim host (original host only) | + +### Feature Client Getters + +| Method | Returns | Description | +|--------|---------|-------------| +| `getMediaStream()` | `Stream` | Get media stream (AFTER join!) | +| `getChatClient()` | `ChatClient` | Get chat client | +| `getCommandClient()` | `CommandChannel` | Get command channel | +| `getRecordingClient()` | `RecordingClient` | Get recording client | +| `getLiveTranscriptionClient()` | `LiveTranscriptionClient` | Get transcription client | +| `getLiveStreamClient()` | `LiveStreamClient` | Get live stream client | +| `getSubsessionClient()` | `SubsessionClient` | Get subsession client | +| `getWhiteboardClient()` | `WhiteboardClient` | Get whiteboard client | +| `getBroadcastStreamingClient()` | `BroadcastStreamingClient` | Get broadcast client | +| `getRealTimeMediaStreamsClient()` | `RealTimeMediaStreamsClient` | Get RTMS client | +| `getLoggerClient(options?)` | `LoggerClient` | Get logger client | + +### Participant Interface + +```typescript +interface Participant { + userId: number; // Unique user ID + displayName: string; // Display name + bVideoOn: boolean; // Is video on + muted: boolean; // Is audio muted + audio: '' | 'computer' | 'phone'; // Audio type + sharerOn: boolean; // Is sharing screen + bShareAudioOn: boolean; // Is sharing audio + isHost: boolean; // Is host +} +``` + +--- + +## Level 2: Stream (Media Operations) + +### Video Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startVideo` | `(options?)` | `ExecutedResult` | Start camera | +| `stopVideo` | `()` | `ExecutedResult` | Stop camera | +| `attachVideo` | `(userId, quality, element?)` | `Promise` | Attach video to DOM | +| `detachVideo` | `(userId, element?)` | `Promise` | Detach video | +| `switchCamera` | `(cameraId)` | `ExecutedResult` | Switch camera | +| `getCameraList` | `()` | `MediaDevice[]` | Get cameras | +| `getActiveCamera` | `()` | `string` | Get active camera ID | +| `mirrorVideo` | `(enable)` | `ExecutedResult` | Mirror video | +| `spotlightVideo` | `(userId)` | `ExecutedResult` | Spotlight user | +| `screenshotVideo` | `(userId?)` | `Promise` | Screenshot video | + +### Video Capability Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `isSupportHDVideo()` | `boolean` | Is HD video supported | +| `getVideoMaxQuality()` | `VideoQuality` | Get max video quality | +| `getMaxRenderableVideos()` | `number` | Max renderable videos | +| `isSupportMultipleVideos()` | `boolean` | Multiple videos support | +| `isSupportVirtualBackground()` | `boolean` | Virtual BG support | +| `isCapturingVideo()` | `boolean` | Is capturing video | + +### VideoQuality Enum + +```typescript +enum VideoQuality { + Video_90P = 0, + Video_180P = 1, + Video_360P = 2, + Video_720P = 3, + Video_1080P = 4 +} +``` + +### Virtual Background Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `updateVirtualBackgroundImage` | `(image)` | `ExecutedResult` | Set virtual background | +| `previewVirtualBackground` | `(canvas, image)` | `ExecutedResult` | Preview virtual BG | +| `stopPreviewVirtualBackground` | `()` | `ExecutedResult` | Stop preview | +| `getVirtualbackgroundStatus` | `()` | `VirtualBackgroundStatus` | Get VB status | + +**Virtual Background Options:** +- `'blur'`: Blur background +- `'https://example.com/image.jpg'`: Custom image URL +- `undefined`: Remove virtual background + +### Audio Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startAudio` | `(options?)` | `ExecutedResult` | Start audio | +| `stopAudio` | `()` | `ExecutedResult` | Stop audio | +| `muteAudio` | `(userId?)` | `ExecutedResult` | Mute audio | +| `unmuteAudio` | `(userId?)` | `ExecutedResult` | Unmute audio | +| `muteAllAudio` | `()` | `ExecutedResult` | Mute all (host) | +| `unmuteAllAudio` | `()` | `ExecutedResult` | Unmute all (host) | +| `switchMicrophone` | `(micId)` | `ExecutedResult` | Switch microphone | +| `switchSpeaker` | `(speakerId)` | `ExecutedResult` | Switch speaker | +| `getMicList` | `()` | `MediaDevice[]` | Get microphones | +| `getSpeakerList` | `()` | `MediaDevice[]` | Get speakers | +| `getActiveMicrophone` | `()` | `string` | Get active mic ID | +| `getActiveSpeaker` | `()` | `string` | Get active speaker ID | +| `isAudioMuted` | `(userId?)` | `boolean` | Is audio muted | + +### Screen Share Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startShareScreen` | `(canvas, options?)` | `ExecutedResult` | Start sharing | +| `stopShareScreen` | `()` | `ExecutedResult` | Stop sharing | +| `startShareView` | `(canvas, userId)` | `ExecutedResult` | View share | +| `stopShareView` | `()` | `ExecutedResult` | Stop viewing | +| `attachShareView` | `(userId, element?)` | `Promise` | Attach share view | +| `detachShareView` | `(userId, element?)` | `Promise` | Detach share view | +| `pauseShareScreen` | `()` | `ExecutedResult` | Pause share | +| `resumeShareScreen` | `()` | `ExecutedResult` | Resume share | +| `getActiveShareUserId` | `()` | `number` | Get sharer user ID | +| `getShareStatus` | `()` | `ShareStatus` | Get share status | +| `getShareUserList` | `()` | `Participant[]` | Get sharers | +| `lockShare` | `(isLocked)` | `ExecutedResult` | Lock share (host) | +| `setSharePrivilege` | `(privilege)` | `ExecutedResult` | Set share privilege | +| `isStartShareScreenWithVideoElement` | `()` | `boolean` | Use video or canvas | + +### ScreenShareOption Interface + +```typescript +interface ScreenShareOption { + requestReadReceipt?: boolean; // Request read receipt + secondaryAudio?: boolean; // Share with audio + optimizedForVideo?: boolean; // Optimize for video +} +``` + +### ShareStatus Enum + +```typescript +enum ShareStatus { + Sharing = 'Sharing', + Paused = 'Paused', + End = 'End' +} +``` + +### Processor Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `createProcessor` | `(params)` | `Promise` | Create processor | +| `addProcessor` | `(processor)` | `Promise<"">` | Add processor | +| `removeProcessor` | `(processor)` | `Promise<"">` | Remove processor | +| `isSupportVideoProcessor` | `()` | `boolean` | Video processor support | +| `isSupportAudioProcessor` | `()` | `boolean` | Audio processor support | +| `isSupportShareProcessor` | `()` | `boolean` | Share processor support | + +--- + +## Level 2: Feature Clients + +### ChatClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `send` | `(message)` | `ExecutedResult` | Send to all | +| `sendToUser` | `(userId, message)` | `ExecutedResult` | Send to user | +| `sendFile` | `(file, receiverId)` | `ExecutedResult` | Send file | +| `downloadFile` | `(fileUrl, options)` | `ExecutedResult` | Download file | + +### CommandChannel + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `send` | `(text)` | `ExecutedResult` | Send to all | +| `sendToUser` | `(userId, text)` | `ExecutedResult` | Send to user | + +### RecordingClient + +| Method | Returns | Description | +|--------|---------|-------------| +| `startCloudRecording()` | `ExecutedResult` | Start recording (host) | +| `stopCloudRecording()` | `ExecutedResult` | Stop recording | +| `pauseCloudRecording()` | `ExecutedResult` | Pause recording | +| `resumeCloudRecording()` | `ExecutedResult` | Resume recording | + +### LiveTranscriptionClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startLiveTranscription` | `()` | `ExecutedResult` | Start transcription | +| `stopLiveTranscription` | `()` | `ExecutedResult` | Stop transcription | +| `enableReceivingCaption` | `(enable)` | `ExecutedResult` | Enable/disable captions | +| `setSpokenLanguage` | `(language)` | `ExecutedResult` | Set spoken language | + +### LiveStreamClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startLiveStream` | `(url, key)` | `ExecutedResult` | Start streaming | +| `stopLiveStream` | `()` | `ExecutedResult` | Stop streaming | + +### SubsessionClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `createSubsessions` | `(names)` | `ExecutedResult` | Create subsessions | +| `openSubsessions` | `(rooms)` | `ExecutedResult` | Open subsessions | +| `closeAllSubsessions` | `()` | `ExecutedResult` | Close all | +| `broadcast` | `(message)` | `ExecutedResult` | Broadcast message | +| `getSubsessionList` | `()` | `Subsession[]` | Get subsessions | + +--- + +## Events Reference + +### Session Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `connection-change` | `ConnectionChangePayload` | Connection state changed | +| `user-added` | `ParticipantPropertiesPayload[]` | Participant joined | +| `user-removed` | `ParticipantPropertiesPayload[]` | Participant left | +| `user-updated` | `ParticipantPropertiesPayload[]` | Participant updated | + +### Video Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `peer-video-state-change` | `{action: 'Start'\|'Stop', userId}` | Peer video on/off | +| `video-active-change` | `{state: VideoActiveState, userId}` | Video stream changed | +| `video-capturing-change` | `{state: VideoCapturingState}` | Capture state changed | +| `video-dimension-change` | `{width, height, type}` | Video dimensions changed | + +### Audio Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `current-audio-change` | `{action, source?, type?}` | Audio state changed | +| `active-speaker` | `ActiveSpeaker[]` | Active speakers | +| `host-ask-unmute-audio` | `{reason}` | Host asks unmute | +| `auto-play-audio-failed` | (none) | Auto-play blocked | + +### Screen Share Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `active-share-change` | `{state: 'Active'\|'Inactive', userId}` | Share active state | +| `peer-share-state-change` | `{action: 'Start'\|'Stop', userId}` | Peer share changed | +| `passively-stop-share` | `PassiveStopShareReason` | Share stopped passively | +| `share-content-dimension-change` | `{width, height, type}` | Share size changed | + +### Chat Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `chat-on-message` | `ChatMessage` | Message received | +| `chat-privilege-change` | `{chatPrivilege}` | Privilege changed | + +### Command Channel Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `command-channel-message` | `{senderId, senderName, text, timestamp}` | Command received | +| `command-channel-status` | `ConnectionState` | Channel status | + +### Recording Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `recording-change` | `{state: RecordingStatus}` | Recording state changed | +| `individual-recording-change` | `{state, userId?}` | Individual recording | + +### Transcription Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `caption-message` | `LiveTranscriptionMessage` | Caption received | +| `caption-status` | `{autoCaption, lang?, ...}` | Caption status | +| `caption-enable` | `boolean` | Caption enabled/disabled | + +### Media Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `device-change` | (none) | Device added/removed | +| `device-permission-change` | `{name, state}` | Permission changed | +| `network-quality-change` | `{level, type, userId}` | Network quality | + +--- + +## Error Types + +```typescript +type ErrorTypes = + | 'INVALID_OPERATION' // Duplicated operation + | 'INTERNAL_ERROR' // Service unavailable + | 'OPERATION_TIMEOUT' // Timed out + | 'INSUFFICIENT_PRIVILEGES' // Need host/manager + | 'IMPROPER_MEETING_STATE' // Not in meeting + | 'INVALID_PARAMETERS' // Wrong params + | 'OPERATION_LOCKED'; // Property locked +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Navigation guide +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal pattern +- [SKILL.md](../SKILL.md) - Main skill overview diff --git a/partner-built/zoom-plugin/skills/video-sdk/web/references/web.md b/partner-built/zoom-plugin/skills/video-sdk/web/references/web.md new file mode 100644 index 00000000..e3a6e548 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/web/references/web.md @@ -0,0 +1,1017 @@ +# Video SDK - Web + +Build custom video experiences in the browser with Zoom Video SDK. + +## Overview + +The Zoom Video SDK for Web enables fully customized video applications using Zoom's infrastructure. You control the UI, branding, and user experience. + +## Prerequisites + +- Video SDK credentials from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- SDK Key and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/videosdk +``` + +### CDN (Fallback Strategy) + +> **Note**: Some networks/ad blockers can block `source.zoom.us`. Prefer allowlisting the domain in managed environments. If you need a fallback, consider mirroring/self-hosting only if permitted and you can keep versions in sync. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-1.12.0.min.js" -o public/js/zoom-video-sdk.min.js +``` + +```html + + +``` + +**Note:** Remember to update your local copy when new SDK versions are released. + +## Quick Start + +### NPM Usage (Bundler) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// CRITICAL: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### CDN Usage (No Bundler) + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +// Must use .default property +const ZoomVideo = WebVideoSDK.default; +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// CRITICAL: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### ES Module with CDN (Race Condition) + +When using ` +``` + +**Solution 2 - Wait for SDK to load**: +```javascript +function waitForSDK(timeout = 10000) { + return new Promise((resolve, reject) => { + if (typeof WebVideoSDK !== 'undefined') { + resolve(); + return; + } + const start = Date.now(); + const check = setInterval(() => { + if (typeof WebVideoSDK !== 'undefined') { + clearInterval(check); + resolve(); + } else if (Date.now() - start > timeout) { + clearInterval(check); + reject(new Error('SDK failed to load')); + } + }, 100); + }); +} + +await waitForSDK(); +const ZoomVideo = WebVideoSDK.default; +``` + +### 5. CDN exports WebVideoSDK.default, not ZoomVideo + +**Symptom**: `ZoomVideo.createClient()` fails with CDN + +**Cause**: CDN exports as `WebVideoSDK`, not `ZoomVideo` + +**Solution**: +```javascript +// NPM +import ZoomVideo from '@zoom/videosdk'; + +// CDN +const ZoomVideo = WebVideoSDK.default; // Note: .default! + +const client = ZoomVideo.createClient(); +``` + +### 6. Join Fails with "Invalid signature" + +**Symptom**: `join()` throws error about invalid signature + +**Causes**: +1. JWT expired (check `exp` claim) +2. JWT malformed +3. Wrong SDK key/secret +4. Topic doesn't match JWT `tpc` claim + +**Solution**: +1. Generate JWT on server side +2. Check JWT expiration (typically 24h) +3. Verify topic matches JWT `tpc` value +4. Verify SDK key is correct + +### 7. Camera/Microphone Permission Denied + +**Symptom**: `startVideo()` or `startAudio()` fails + +**Cause**: Browser permission denied + +**Solution**: +```javascript +// Check permissions before starting +try { + await stream.startVideo(); +} catch (error) { + if (error.type === 'INSUFFICIENT_PRIVILEGES') { + // Permission denied - guide user + alert('Please allow camera access in browser settings'); + } +} +``` + +### 8. HD Video Not Working + +**Symptom**: Video quality stays at 360p despite `{ hd: true }` + +**Causes**: +1. SharedArrayBuffer not available +2. Browser doesn't support HD +3. Network conditions + +**Solution**: +```javascript +// Check HD support +if (stream.isSupportHDVideo()) { + await stream.startVideo({ hd: true }); +} else { + console.warn('HD not supported'); + await stream.startVideo(); +} + +// Check SharedArrayBuffer +const sabAvailable = typeof SharedArrayBuffer === 'function'; +if (!sabAvailable) { + console.warn('SharedArrayBuffer not available - add COOP/COEP headers'); +} +``` + +**Server Headers for SharedArrayBuffer**: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +### 9. Screen Share Element Type Error + +**Symptom**: `startShareScreen()` fails or shows nothing + +**Cause**: Using wrong element type (video vs canvas) + +**Solution**: +```javascript +// Check which element type to use +if (stream.isStartShareScreenWithVideoElement()) { + const video = document.getElementById('share-video'); + await stream.startShareScreen(video as unknown as HTMLCanvasElement); +} else { + const canvas = document.getElementById('share-canvas'); + await stream.startShareScreen(canvas); +} +``` + +### 10. CORS Error to log-external-gateway.zoom.us + +**Symptom**: Console shows CORS errors to Zoom telemetry + +**Cause**: COOP/COEP headers blocking telemetry + +**Impact**: None - harmless. SDK works fine. + +**Solution**: Ignore these errors. They're telemetry-related and don't affect functionality. + +--- + +## Error Types Reference + +| Error Type | Meaning | Common Cause | +|------------|---------|--------------| +| `INVALID_OPERATION` | Duplicated operation | Calling same method twice | +| `INTERNAL_ERROR` | Service unavailable | Network issues | +| `OPERATION_TIMEOUT` | Timed out | Slow connection | +| `INSUFFICIENT_PRIVILEGES` | Need host/manager | Not authorized | +| `IMPROPER_MEETING_STATE` | Not in meeting | Wrong lifecycle stage | +| `INVALID_PARAMETERS` | Wrong params | Bad user ID, etc. | +| `OPERATION_LOCKED` | Property locked | Feature disabled | + +--- + +## Browser-Specific Issues + +### Safari + +| Issue | Solution | +|-------|----------| +| Virtual background not supported | Use alternative (blur not available) | +| Screen sharing requires macOS 15+ | Use Chrome/Firefox | +| Some audio issues | Enable `patchJsMedia: true` | + +### Firefox + +| Issue | Solution | +|-------|----------| +| Virtual background requires 90+ | Update Firefox | +| Some WebRTC issues | Use Chrome if critical | + +### Mobile Browsers + +| Issue | Solution | +|-------|----------| +| Limited screen share | Use desktop for sharing | +| Performance issues | Lower video quality | +| Camera switching | Use `MobileVideoFacingMode` enum | + +--- + +## Debugging Tips + +### 1. Enable SDK Logging + +```javascript +const loggerClient = client.getLoggerClient({ + level: 'debug' +}); +``` + +### 2. Check Event Flow + +```javascript +// Log all events +['connection-change', 'user-added', 'user-removed', 'peer-video-state-change'].forEach(event => { + client.on(event, (payload) => { + console.log(`Event: ${event}`, payload); + }); +}); +``` + +### 3. Check Participant State + +```javascript +const users = client.getAllUser(); +console.table(users.map(u => ({ + userId: u.userId, + name: u.displayName, + videoOn: u.bVideoOn, + muted: u.muted, + audio: u.audio +}))); +``` + +### 4. Check Stream State + +```javascript +console.log('Active camera:', stream.getActiveCamera()); +console.log('Active mic:', stream.getActiveMicrophone()); +console.log('Capturing video:', stream.isCapturingVideo()); +console.log('Audio muted:', stream.isAudioMuted()); +console.log('HD supported:', stream.isSupportHDVideo()); +console.log('Max quality:', stream.getVideoMaxQuality()); +``` + +--- + +## Real-World Integration Pitfalls (Custom Waiting Room Flows) + +These came up in production-style waiting-room to main-session transfers. + +### A) Joined, but no audio/video works on Firefox + +**Symptom**: Session joins, but media pipeline is flaky or blank. + +**Cause**: CSP blocks WebAssembly execution used by `js_media.min.js`. + +**Fix**: Ensure CSP `script-src` includes: + +```text +'wasm-unsafe-eval' 'unsafe-eval' +``` + +Also keep required Zoom domains in `script-src` and allow `worker-src blob:`. + +### B) Transfer works, but customer remote video never appears + +**Symptom**: Customer reaches main session but does not see advisor video. + +**Likely causes**: +1. Advisor is not publishing video (`bVideoOn` is false) +2. Event listener race during waiting->main rejoin +3. Attach attempted too early during stream readiness + +**Fix pattern**: +- Bind listeners once and gate logic by current session mode. +- On main join, do both: + - immediate `getAllUser()` render pass + - short retry/poll window for late stream availability +- Handle `peer-video-state-change`, `user-added`, and `user-updated`. + +### C) Self video appears at wrong page position + +**Symptom**: Self video renders far down the page instead of in tile. + +**Cause**: Container CSS/DOM mismatch for SDK inserted elements. + +**Fix**: +- Use `video-player-container` for SDK video mounts. +- Ensure child elements are explicitly sized: + +```css +video-player-container video-player, +video-player-container canvas, +video-player-container video { + width: 100%; + height: 100%; + display: block; +} +``` + +### D) Command channel transfer message is "missed" + +**Symptom**: Admit clicked, but customer does not transfer. + +**Cause**: Command channel does not replay history. If customer wasn't fully in waiting session yet, message is missed. + +**Fix**: +- Keep backend transfer state and allow customer to fetch transfer details after join. +- Consider one-time transfer lookup on customer waiting join as race guard. + +### E) Repeated CORS errors to `log-external-gateway.zoom.us` + +**Symptom**: Console spam with CORS 531 errors. + +**Impact**: Usually telemetry-only; does not block core session/media. + +**Action**: Treat as noise unless accompanied by actual join or media API failures. + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Lifecycle order +- [Video Rendering](../examples/video-rendering.md) - attachVideo patterns +- [Event Handling](../examples/event-handling.md) - Required events +- [SKILL.md](../SKILL.md) - Quick reference diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/RUNBOOK.md b/partner-built/zoom-plugin/skills/video-sdk/windows/RUNBOOK.md new file mode 100644 index 00000000..adbc167d --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK Windows 5-Minute Preflight Runbook + +Use this before deep debugging. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- SDK/API names can drift by version; validate current names against docs/raw-docs before release. + +## 1) Confirm Integration Surface + +- Confirm this is a Video SDK custom session flow for Windows (not Meeting SDK). +- Verify UI/state are driven by session events, not meeting semantics. +- Wrapper platforms require JS/native bridge synchronization checks. + +## 2) Confirm Required Credentials + +- Video SDK app credentials (SDK Key/Secret) stored server-side. +- Backend-generated session JWT token. +- Session fields (`sessionName`, `userName`, role type) resolved before join. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK client/context and register event listeners. +2. Generate/fetch session token from backend. +3. Join session and establish media streams. +4. Handle participant/media/control events during active session. + +## 4) Confirm Event/State Handling + +- Keep participant state keyed by user/session IDs. +- Reconcile subscribe/unsubscribe transitions for video/audio/share streams. +- Treat reconnect and device-change events as first-class state transitions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave/end session and release helper/client resources. +- Remove listeners to avoid duplicate callbacks on rejoin. +- Re-check SDK version compatibility before deployment updates. + +## 6) Quick Probes + +- Token issuance and join flow succeed once end-to-end. +- Audio/video publish-subscribe operations complete with expected callbacks. +- Leave/rejoin works without leaked listener or stream state. + +## 7) Fast Decision Tree + +- Join fails immediately -> invalid/expired token or session field mismatch. +- Media state stuck -> listener binding/order issue or permission/device problem. +- Inconsistent behavior after update -> wrapper/native SDK version mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/video-sdk/windows/ +- https://marketplacefront.zoom.us/sdk/custom/windows/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/video-sdk/windows/` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/windows/` diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/SKILL.md b/partner-built/zoom-plugin/skills/video-sdk/windows/SKILL.md new file mode 100644 index 00000000..1c711dd8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/SKILL.md @@ -0,0 +1,1019 @@ +--- +name: video-sdk/windows +description: "Zoom Video SDK for Windows - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication" +user-invocable: false +triggers: + - "video sdk windows" + - "windows video sdk" + - "video sdk raw data windows" + - "windows custom video" + - "c++ video sdk" +--- + +# Zoom Video SDK - Windows Development + +Expert guidance for developing with the Zoom Video SDK on Windows. This SDK enables custom video applications, raw media capture/injection, cloud recording, live streaming, and real-time transcription on Windows platforms. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/windows/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +**Sample Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +## Quick Links + +**New to Video SDK? Follow this path:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +2. **[Session Join Pattern](examples/session-join-pattern.md)** - Complete working code to join a session +3. **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - **CRITICAL**: Fix callbacks not firing +4. **[Video Rendering](examples/video-rendering.md)** - Display video with Canvas API + +**Reference:** +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 5-level SDK navigation map +- **[API Reference](references/windows-reference.md)** - Methods, error codes, timing rules +- **[Delegate Methods](references/delegate-methods.md)** - All 80+ callback methods +- **[Sample Applications](references/samples.md)** - Official samples guide +- **[windows.md](windows.md)** - Secondary overview doc (pointer-style) +- **[SKILL.md](SKILL.md)** - Complete documentation navigation + +**Having issues?** +- Callbacks not firing → [Windows Message Loop](troubleshooting/windows-message-loop.md) +- Build errors → [Build Errors Guide](troubleshooting/build-errors.md) +- Video subscribe fails → [Video Rendering](examples/video-rendering.md) (subscribe in `onUserVideoStatusChanged`) +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) + +**Building a Custom UI?** +- [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) - Choose your rendering approach +- [Raw Video Capture](examples/raw-video-capture.md) - YUV420 frame processing + +## SDK Overview + +The Zoom Video SDK for Windows is a C++ library that provides: +- **Session Management**: Join/leave video SDK sessions +- **Raw Data Access**: Capture raw audio/video frames (YUV420, PCM) +- **Raw Data Injection**: Send custom audio/video into sessions +- **Screen Sharing**: Share screens or inject custom share sources +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints (YouTube, etc.) +- **Chat & Commands**: In-session messaging and command channels +- **Live Transcription**: Real-time speech-to-text +- **Subsessions**: Breakout room support +- **Whiteboard**: Collaborative whiteboard features +- **Annotations**: Screen share annotations +- **C# Integration**: C++/CLI wrapper for .NET applications + +## Prerequisites + +### System Requirements + +- **OS**: Windows 10 (1903 or later) or Windows 11 +- **Architecture**: x64 (recommended), x86, or ARM64 +- **Visual Studio**: 2019 or 2022 (Community, Professional, or Enterprise) +- **Windows SDK**: 10.0.19041.0 or later +- **.NET Framework**: 4.8 or later (for C# applications) + +### Visual Studio Workloads + +Install these workloads via Visual Studio Installer: + +1. **Desktop development with C++** + - MSVC v142 or v143 compiler + - Windows 10/11 SDK + - C++ CMake tools (optional) + +2. **.NET desktop development** (for C# applications) + - .NET Framework 4.8 targeting pack + - C++/CLI support + +## Quick Start + +### C++ Application + +```cpp +#include +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +// 1. Create SDK object +IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj(); + +// 2. Initialize +ZoomVideoSDKInitParams init_params; +init_params.domain = L"https://zoom.us"; +init_params.enableLog = true; +init_params.logFilePrefix = L"zoom_win_video"; +init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + +ZoomVideoSDKErrors err = video_sdk_obj->initialize(init_params); + +// 3. Add event listener +video_sdk_obj->addListener(myDelegate); + +// 4. Join session (IMPORTANT: set audioOption.connect = false) +ZoomVideoSDKSessionContext session_context; +session_context.sessionName = L"my-session"; +session_context.userName = L"Windows User"; +session_context.token = L"your-jwt-token"; +session_context.videoOption.localVideoOn = false; +session_context.audioOption.connect = false; // Connect audio after join +session_context.audioOption.mute = true; + +IZoomVideoSDKSession* session = video_sdk_obj->joinSession(session_context); + +// 5. CRITICAL: Add Windows message pump for callbacks to work +bool running = true; +while (running) { + // Process Windows messages (required for SDK callbacks) + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Your application logic here + Sleep(10); +} +``` + +### C# Application + +```csharp +using ZoomVideoSDK; + +var sdkManager = new ZoomSDKManager(); +sdkManager.Initialize(); +sdkManager.JoinSession("my-session", "jwt-token", "User Name", ""); +``` + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Session Management** | Join, leave, and manage video sessions | +| **Raw Video (YUV I420)** | Capture and inject raw video frames | +| **Raw Audio (PCM)** | Capture and inject raw audio data | +| **Screen Sharing** | Share screens or custom content | +| **Cloud Recording** | Record sessions to Zoom cloud | +| **Live Streaming** | Stream to RTMP endpoints | +| **Chat** | Send/receive chat messages | +| **Command Channel** | Custom command messaging | +| **Live Transcription** | Real-time speech-to-text | +| **C# Support** | Full .NET Framework integration | + +## Sample Applications + +**Official Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +| Sample | Description | +|--------|-------------| +| VSDK_SkeletonDemo | Minimal session join - **start here** | +| VSDK_getRawVideo | Capture YUV420 video frames | +| VSDK_getRawAudio | Capture PCM audio | +| VSDK_sendRawVideo | Inject custom video (virtual camera) | +| VSDK_sendRawAudio | Inject custom audio (virtual mic) | +| VSDK_CloudRecording | Cloud recording control | +| VSDK_CommandChannel | Custom command messaging | +| VSDK_TranscriptionAndTranslation | Live captions | + +**See complete guide**: [Sample Applications Reference](references/samples.md) + +## Critical Gotchas and Best Practices + +### ⚠️ CRITICAL: Windows Message Pump Required + +**The #1 issue that causes session joins to hang with no callbacks:** + +All Windows applications using the Zoom SDK **MUST** process Windows messages. The SDK uses Windows messages to deliver callbacks like `onSessionJoin()`, `onError()`, etc. + +**Problem**: Without a message pump, `joinSession()` appears to succeed but callbacks never fire. + +**Solution**: Add this to your main loop: + +```cpp +while (running) { + // REQUIRED: Process Windows messages + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Your application logic + Sleep(10); +} +``` + +**Applies to**: +- Console applications (no automatic message pump) +- Custom main loops +- Applications that don't use standard WinMain/WndProc + +**GUI applications** using WinMain with standard message loop already have this. + +### Audio Connection Strategy + +**Best Practice**: Set `audioOption.connect = false` when joining, then connect audio in the `onSessionJoin()` callback. + +```cpp +// During join +session_context.audioOption.connect = false; // Don't connect yet +session_context.audioOption.mute = true; + +// In onSessionJoin() callback +void onSessionJoin() override { + IZoomVideoSDKAudioHelper* audioHelper = video_sdk_obj->getAudioHelper(); + if (audioHelper) { + audioHelper->startAudio(); // Connect now + } +} +``` + +**Why**: This pattern is used in all official Zoom samples. It separates session join from audio initialization for better reliability and error handling. + +### All Delegate Callbacks Must Be Implemented + +The `IZoomVideoSDKDelegate` interface has 70+ pure virtual methods. **ALL must be implemented**, even if empty: + +```cpp +// Required even if you don't use them +void onProxyDetectComplete() override {} +void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser*, IZoomVideoSDKWhiteboardHelper*) override {} +// ... etc +``` + +**Tip**: Check the SDK version's `zoom_video_sdk_delegate_interface.h` for the complete list. The interface changes between SDK versions. + +### Memory Mode for Raw Data + +Always use heap mode for raw data memory: + +```cpp +init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +``` + +Stack mode can cause issues with large video frames. + +### Thread Safety + +SDK callbacks execute on SDK threads, not your main thread: +- Don't perform heavy operations in callbacks +- Don't call `cleanup()` from within callbacks +- Use thread-safe queues for passing data to UI thread +- Use mutexes when accessing shared state + +### Consult Official Samples First + +When SDK behavior is unexpected, **always check the official samples** before troubleshooting: + +**Local samples**: +- `C:\tempsdk\Zoom_VideoSDK_Windows_RawDataDemos\VSDK_SkeletonDemo\` (simplest) +- `C:\tempsdk\sdksamples\zoom-video-sdk-windows-2.4.12\Sample-Libs\x64\demo\` + +Official samples show correct patterns for: +- Message pump implementation ✓ +- Audio connection strategy ✓ +- Error handling ✓ +- Memory management ✓ + +## Video Rendering - Two Approaches + +The Zoom SDK provides **two different ways** to render video. Choose based on your needs. + +### 🎯 Canvas API (Recommended for Most Use Cases) + +**Best for**: Standard applications, clean video quality, ease of implementation + +The SDK renders video directly to your HWND. **No YUV conversion needed**. + +```cpp +// Subscribe to a user's video with Canvas API +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +if (canvas) { + ZoomVideoSDKErrors ret = canvas->subscribeWithView( + hwnd, // Your window handle + ZoomVideoSDKVideoAspect_PanAndScan, // Fit to window, may crop + ZoomVideoSDKResolution_Auto // Let SDK choose best resolution + ); + + if (ret == ZoomVideoSDKErrors_Success) { + // SDK is now rendering directly to your window! + } +} + +// Unsubscribe when done +canvas->unSubscribeWithView(hwnd); +``` + +**Advantages**: +- ✅ **Best quality** - SDK uses optimized, hardware-accelerated rendering +- ✅ **No artifacts** - Professional video quality +- ✅ **Simple code** - 3 lines to subscribe +- ✅ **Better performance** - No CPU-intensive YUV conversion +- ✅ **Automatic scaling** - SDK handles window resizing +- ✅ **Aspect ratio** - Built-in aspect ratio handling + +**Example from official .NET sample**: +```cpp +// Self video preview +IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas(); +canvas->subscribeWithView(selfVideoHwnd, aspect, resolution); + +// Remote user video +IZoomVideoSDKCanvas* remoteCanvas = remoteUser->GetVideoCanvas(); +remoteCanvas->subscribeWithView(remoteVideoHwnd, aspect, resolution); +``` + +**Video Aspect Options**: +- `ZoomVideoSDKVideoAspect_Original` - Letterbox/pillarbox, no cropping +- `ZoomVideoSDKVideoAspect_FullFilled` - Fill window, may crop edges +- `ZoomVideoSDKVideoAspect_PanAndScan` - Smart crop to fill window +- `ZoomVideoSDKVideoAspect_LetterBox` - Show full video with black bars + +**Resolution Options**: +- `ZoomVideoSDKResolution_90P` +- `ZoomVideoSDKResolution_180P` +- `ZoomVideoSDKResolution_360P` - Good balance +- `ZoomVideoSDKResolution_720P` - HD quality +- `ZoomVideoSDKResolution_1080P` +- `ZoomVideoSDKResolution_Auto` - Let SDK decide (recommended) + +### 🔧 Raw Data Pipe (Advanced Use Cases) + +**Best for**: Custom video processing, effects, recording, computer vision + +You receive raw YUV420 frames and handle rendering yourself. + +```cpp +// 1. Create a delegate to receive frames +class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Convert YUV420 to RGB and render + ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height); + RenderToWindow(rgbBuffer, width, height); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + // Handle video on/off + } +}; + +// 2. Subscribe to raw data +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +VideoRenderer* renderer = new VideoRenderer(); +pipe->subscribe(ZoomVideoSDKResolution_720P, renderer); +``` + +**YUV420 to RGB Conversion** (ITU-R BT.601): +```cpp +void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int yIndex = y * width + x; + int uvIndex = (y / 2) * (width / 2) + (x / 2); + + int Y = (unsigned char)yBuffer[yIndex]; + int U = (unsigned char)uBuffer[uvIndex]; + int V = (unsigned char)vBuffer[uvIndex]; + + // YUV to RGB conversion + int C = Y - 16; + int D = U - 128; + int E = V - 128; + + int R = (298 * C + 409 * E + 128) >> 8; + int G = (298 * C - 100 * D - 208 * E + 128) >> 8; + int B = (298 * C + 516 * D + 128) >> 8; + + // Clamp to [0, 255] + R = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store RGB (BGR format for Windows) + rgbBuffer[yIndex * 3 + 0] = (unsigned char)B; + rgbBuffer[yIndex * 3 + 1] = (unsigned char)G; + rgbBuffer[yIndex * 3 + 2] = (unsigned char)R; + } + } +} +``` + +**Render with GDI**: +```cpp +void RenderToWindow(unsigned char* rgbBuffer, int width, int height) { + HDC hdc = GetDC(hwnd); + + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; // Negative for top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 24; // 24-bit RGB + bmi.bmiHeader.biCompression = BI_RGB; + + RECT rect; + GetClientRect(hwnd, &rect); + + StretchDIBits(hdc, + 0, 0, rect.right, rect.bottom, // Destination + 0, 0, width, height, // Source + rgbBuffer, &bmi, + DIB_RGB_COLORS, SRCCOPY); + + ReleaseDC(hwnd, hdc); +} +``` + +**Disadvantages**: +- ⚠️ **CPU intensive** - YUV conversion can cause frame drops +- ⚠️ **Artifacts** - Manual rendering may show tearing/artifacts +- ⚠️ **Complex** - More code to maintain +- ⚠️ **Performance** - Slower than Canvas API + +**Use Raw Data When**: +- Adding video filters/effects +- Recording to custom formats +- Computer vision processing +- Custom compositing +- Streaming to non-standard outputs + +### Self Video vs Remote Users + +**Self Video** (your own camera): + +**Option A: Canvas API** +```cpp +IZoomVideoSDKSession* session = sdk->getSessionInfo(); +IZoomVideoSDKUser* myself = session->getMyself(); +IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas(); +canvas->subscribeWithView(selfVideoHwnd, aspect, resolution); +``` + +**Option B: Video Preview** (for self only) +```cpp +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); +videoHelper->startVideo(); // Start transmission + +// For preview rendering +videoHelper->startVideoCanvasPreview(selfVideoHwnd, aspect, resolution); +``` + +**Remote Users** (other participants): + +**Canvas API** (recommended): +```cpp +// In onUserJoin callback +void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector* userList) { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + canvas->subscribeWithView(userVideoHwnd, aspect, resolution); + } +} +``` + +### Event-Driven Subscription Pattern + +⚠️ **CRITICAL**: Video subscription must be **event-driven** and **manual**. + +**Key Events**: + +1. **`onSessionJoin`** - Subscribe to self video +2. **`onUserJoin`** - Subscribe to new remote users +3. **`onUserVideoStatusChanged`** - Re-subscribe when video turns on/off +4. **`onUserLeave`** - Unsubscribe and cleanup + +**Complete Pattern**: + +```cpp +class MainFrame : public IZoomVideoSDKDelegate { +private: + std::map subscribedUsers_; + HWND videoWindow_; + +public: + void onSessionJoin() override { + // Start your own video + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + videoHelper->startVideo(); + + // Subscribe to self video + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + SubscribeToUser(myself); + } + + void onUserJoin(IZoomVideoSDKUserHelper*, + IVideoSDKVector* userList) override { + // Get current user to exclude self + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + // IMPORTANT: Only subscribe to REMOTE users! + if (user != myself) { + SubscribeToUser(user); + } + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user != myself) { + // Re-subscribe when video status changes + SubscribeToUser(user); + } + } + } + + void onUserLeave(IZoomVideoSDKUserHelper*, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + UnsubscribeFromUser(user); + } + } + + void onSessionLeave() override { + // Cleanup all subscriptions + for (auto& pair : subscribedUsers_) { + IZoomVideoSDKCanvas* canvas = pair.second; + if (canvas) { + canvas->unSubscribeWithView(videoWindow_); + } + } + subscribedUsers_.clear(); + } + +private: + void SubscribeToUser(IZoomVideoSDKUser* user) { + if (!user || subscribedUsers_.find(user) != subscribedUsers_.end()) + return; + + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + if (canvas) { + ZoomVideoSDKErrors ret = canvas->subscribeWithView( + videoWindow_, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (ret == ZoomVideoSDKErrors_Success) { + subscribedUsers_[user] = canvas; + } + } + } + + void UnsubscribeFromUser(IZoomVideoSDKUser* user) { + auto it = subscribedUsers_.find(user); + if (it != subscribedUsers_.end()) { + IZoomVideoSDKCanvas* canvas = it->second; + if (canvas) { + canvas->unSubscribeWithView(videoWindow_); + } + subscribedUsers_.erase(it); + } + } +}; +``` + +**Key Points**: +- ✅ Subscribe in response to events (onUserJoin, onUserVideoStatusChanged) +- ✅ Always exclude current user from remote subscriptions +- ✅ Unsubscribe on onUserLeave +- ✅ Clean up all subscriptions on onSessionLeave +- ✅ Track subscriptions in a map for lifecycle management + +### ⚠️ Screen Share Subscription (DIFFERENT from Video!) + +**CRITICAL**: Screen share subscription uses `IZoomVideoSDKShareAction` from the callback, NOT `user->GetShareCanvas()`! + +```cpp +// WRONG - This won't work for remote screen shares! +user->GetShareCanvas()->subscribeWithView(hwnd, ...); + +// CORRECT - Use IZoomVideoSDKShareAction from onUserShareStatusChanged callback +void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) { + // Subscribe to the share using Canvas API + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->subscribeWithView(shareWindow_, + ZoomVideoSDKVideoAspect_Original); + } + } + else if (status == ZoomVideoSDKShareStatus_Stop) { + // Unsubscribe when share stops + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareWindow_); + } + } +} +``` + +**Why is share different from video?** +- **Video**: Each user has one video stream → use `user->GetVideoCanvas()` +- **Share**: A user can have multiple share actions (multi-share) → use `IZoomVideoSDKShareAction*` from callback +- The `IZoomVideoSDKShareAction` object represents a specific share stream and contains the share status, type, and rendering interfaces + +**See also**: [Screen Share Subscription Example](examples/screen-share-subscription.md) + +### Multi-User Video Layout + +For multiple participants, you need **one HWND per user**: + +```cpp +// Create separate windows/panels for each user +HWND selfVideoWindow = CreateWindow(...); // Your video +HWND user1Window = CreateWindow(...); // User 1's video +HWND user2Window = CreateWindow(...); // User 2's video + +// Subscribe each user to their own window +myself->GetVideoCanvas()->subscribeWithView(selfVideoWindow, ...); +user1->GetVideoCanvas()->subscribeWithView(user1Window, ...); +user2->GetVideoCanvas()->subscribeWithView(user2Window, ...); +``` + +**Layout Strategies**: +- Grid layout (2x2, 3x3) +- Gallery view (scrollable) +- Active speaker (large) + thumbnails +- Picture-in-picture + +### Common Video Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Video not showing | Not calling `startVideo()` | Call `videoHelper->startVideo()` in `onSessionJoin` | +| Artifacts/tearing | Using Raw Data Pipe | Switch to Canvas API | +| Poor performance | YUV conversion on UI thread | Use Canvas API or move conversion to worker thread | +| Video freezes | Not processing Windows messages | Add message pump to main loop | +| Can't see self | Subscribing to wrong user | Use `session->getMyself()` for self video | +| Seeing self in remote list | Not excluding self | Check `if (user != myself)` before subscribing | + +## Complete Documentation Library + +This skill includes comprehensive guides organized by category: + +### Core Concepts (Start Here!) +- **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 5-level navigation guide +- **[Canvas vs Raw Data](concepts/canvas-vs-raw-data.md)** - Choose your rendering approach + +### Complete Examples +- **[Session Join Pattern](examples/session-join-pattern.md)** - JWT auth + session join with full code +- **[Video Rendering](examples/video-rendering.md)** - Canvas API video display +- **[Screen Share Subscription](examples/screen-share-subscription.md)** - View remote screen shares (DIFFERENT from video!) +- **[Raw Video Capture](examples/raw-video-capture.md)** - YUV420 frame capture +- **[Raw Audio Capture](examples/raw-audio-capture.md)** - PCM audio capture +- **[Send Raw Video](examples/send-raw-video.md)** - Virtual camera (inject custom video) +- **[Send Raw Audio](examples/send-raw-audio.md)** - Virtual mic (inject custom audio) +- **[Cloud Recording](examples/cloud-recording.md)** - Cloud recording control +- **[Command Channel](examples/command-channel.md)** - Custom command messaging +- **[Transcription](examples/transcription.md)** - Live transcription/captions + +### UI Framework Integration +- **[Win32 Native](examples/dotnet-winforms/README.md#option-1-win32-native-c---direct-sdk)** - Direct SDK usage with Canvas API (best performance) +- **[WinForms (.NET)](examples/dotnet-winforms/README.md#option-2-winforms-c--ccli-wrapper)** - C++/CLI wrapper + Raw Data Pipe +- **[WPF (.NET)](examples/dotnet-winforms/README.md#option-3-wpf-c--ccli-wrapper)** - C++/CLI wrapper + BitmapSource conversion +- **[Production Quality Guidelines](examples/dotnet-winforms/README.md#production-quality-review)** - Checklist and common issues + +### C++/CLI Wrapper Patterns (Wrapping ANY Native Library) +- **[Complete Guide](examples/dotnet-winforms/README.md#ccli-wrapper-patterns-for-net-integration)** - 8 patterns for native→.NET interop +- **[Pattern 1: Basic Structure](examples/dotnet-winforms/README.md#pattern-1-basic-wrapper-structure)** - Project setup, class layout +- **[Pattern 2: void* Pointers](examples/dotnet-winforms/README.md#pattern-2-opaque-void-pointers)** - Hide native types +- **[Pattern 3: gcroot Callbacks](examples/dotnet-winforms/README.md#pattern-3-gcrootT-for-nativemanaged-callbacks)** - Native→Managed events +- **[Pattern 4: IDisposable](examples/dotnet-winforms/README.md#pattern-4-destructor--finalizer-idisposable)** - Cleanup pattern +- **[Pattern 5: Strings](examples/dotnet-winforms/README.md#pattern-5-string-conversion)** - String^ ↔ wstring/string +- **[Pattern 6: Arrays](examples/dotnet-winforms/README.md#pattern-6-arraybuffer-conversion)** - pin_ptr, Marshal::Copy +- **[Pattern 7: Threading](examples/dotnet-winforms/README.md#pattern-7-thread-marshaling-native-thread--ui-thread)** - UI thread dispatch +- **[Pattern 8: LockBits](examples/dotnet-winforms/README.md#pattern-8-lockbits-for-fast-image-manipulation)** - Fast image conversion +- **[Common Errors](examples/dotnet-winforms/README.md#common-wrapper-errors)** - Troubleshooting + +### Troubleshooting +- **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - **CRITICAL**: Why callbacks don't fire +- **[Build Errors](troubleshooting/build-errors.md)** - SDK header dependency fixes +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics & error codes + +### References +- **[API Reference](references/windows-reference.md)** - 5-level API hierarchy, methods, error codes +- **[Delegate Methods](references/delegate-methods.md)** - All 80+ callback methods +- **[SKILL.md](SKILL.md)** - Complete navigation guide + +### Most Critical Issues (From Real Debugging) + +1. **Callbacks not firing** → Missing Windows message loop (99% of issues) + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Video subscribe returns error 2** → Subscribing too early + - See: [Video Rendering](examples/video-rendering.md) - Subscribe in `onUserVideoStatusChanged` + +3. **Abstract class errors** → Missing virtual method implementations + - See: [Delegate Methods](references/delegate-methods.md) + +### Key Insight + +**Once you learn the 3-step pattern, you can implement ANY feature:** +1. Get singleton → 2. Implement delegate → 3. Subscribe & use + +See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Dev Forum**: https://devforum.zoom.us/ +- **GitHub Samples**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Working Sample**: `C:\tempsdk\zoom-video-sdk-windows-sample\` (complete implementation) + +--- + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from video-sdk/windows/SKILL.md + +# Zoom Video SDK Windows - Complete Documentation Index + +## Quick Start Path + +**If you're new to the SDK, follow this order:** + +0. **Overview** → [windows.md](windows.md) +1. **Read the architecture pattern** → [concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md) + - Universal formula: Singleton → Delegate → Subscribe + - Once you understand this, you can implement any feature + +2. **Fix build errors** → [troubleshooting/build-errors.md](troubleshooting/build-errors.md) + - SDK header dependencies + - Required include order + +3. **Implement session join** → [examples/session-join-pattern.md](examples/session-join-pattern.md) + - Complete working JWT + session join code + +4. **Fix callback issues** → [troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md) + - **CRITICAL**: Why callbacks don't fire without Windows message loop + +5. **Implement video** → [examples/video-rendering.md](examples/video-rendering.md) + - Canvas API (SDK-rendered) vs Raw Data Pipe + +6. **Troubleshoot any issues** → [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Quick diagnostic checklist + - Error code tables + +--- + +## Documentation Structure + +``` +video-sdk/windows/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +├── windows.md # Secondary overview doc (pointer-style) +│ +├── concepts/ # Core architectural patterns +│ ├── sdk-architecture-pattern.md # Universal formula for ANY feature +│ ├── singleton-hierarchy.md # 5-level navigation guide +│ └── canvas-vs-raw-data.md # SDK-rendered vs self-rendered choice +│ +├── examples/ # Complete working code +│ ├── session-join-pattern.md # JWT auth + session join +│ ├── video-rendering.md # Canvas API video display +│ ├── screen-share-subscription.md # View remote screen shares +│ ├── raw-video-capture.md # YUV420 raw frame capture +│ ├── raw-audio-capture.md # PCM audio capture +│ ├── send-raw-video.md # Virtual camera (inject video) +│ ├── send-raw-audio.md # Virtual mic (inject audio) +│ ├── cloud-recording.md # Cloud recording control +│ ├── command-channel.md # Custom command messaging +│ ├── transcription.md # Live transcription/captions +│ └── dotnet-winforms/ # UI Framework integration +│ └── README.md # Win32, WinForms, WPF patterns +│ # C++/CLI wrapper patterns +│ # Production quality guidelines +│ +├── troubleshooting/ # Problem solving guides +│ ├── windows-message-loop.md # CRITICAL - Why callbacks fail +│ ├── build-errors.md # Header dependency fixes +│ └── common-issues.md # Quick diagnostic workflow +│ +└── references/ # Reference documentation + ├── windows-reference.md # API hierarchy, methods, error codes + ├── delegate-methods.md # All 80+ callback methods + └── samples.md # Official samples guide +``` + +--- + +## By Use Case + +### I want to build a video app +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Understand the pattern +2. [Session Join Pattern](examples/session-join-pattern.md) - Join sessions +3. [Video Rendering](examples/video-rendering.md) - Display video +4. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Fix callback issues + +### I'm getting build errors +1. [Build Errors Guide](troubleshooting/build-errors.md) - SDK header dependencies +2. [Delegate Methods](references/delegate-methods.md) - Abstract class errors +3. [Common Issues](troubleshooting/common-issues.md) - Linker errors + +### I'm getting runtime errors +1. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callbacks not firing +2. [Common Issues](troubleshooting/common-issues.md) - Error code tables + +### I want to view screen shares +1. [Screen Share Subscription](examples/screen-share-subscription.md) - **DIFFERENT from video!** +2. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Event-driven pattern +3. [Video Rendering](examples/video-rendering.md) - Compare with video subscription + +### I want to capture raw video/audio +1. [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) - Choose your approach +2. [Raw Video Capture](examples/raw-video-capture.md) - YUV420 frame capture +3. [Raw Audio Capture](examples/raw-audio-capture.md) - PCM audio capture +4. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Subscription pattern + +### I want to send custom video/audio (virtual camera/mic) +1. [Send Raw Video](examples/send-raw-video.md) - Inject custom video frames +2. [Send Raw Audio](examples/send-raw-audio.md) - Inject custom audio +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - External source pattern + +### I want to record sessions +1. [Cloud Recording](examples/cloud-recording.md) - Start/stop cloud recording +2. [API Reference](references/windows-reference.md) - Recording helper methods + +### I want to use live transcription +1. [Transcription](examples/transcription.md) - Enable live captions +2. [Delegate Methods](references/delegate-methods.md) - Transcription callbacks + +### I want custom messaging between participants +1. [Command Channel](examples/command-channel.md) - Send custom commands +2. [API Reference](references/windows-reference.md) - Command channel methods + +### I want to build a Win32 native app +1. [Win32 Integration](examples/dotnet-winforms/README.md#option-1-win32-native-c---direct-sdk) - Direct SDK + Canvas API +2. [Video Rendering](examples/video-rendering.md) - Canvas API patterns +3. [Production Guidelines](examples/dotnet-winforms/README.md#production-quality-review) - Best practices + +### I want to build a WinForms (.NET) app +1. [WinForms Integration](examples/dotnet-winforms/README.md#option-2-winforms-c--ccli-wrapper) - C++/CLI wrapper + Raw Data +2. [C++/CLI Patterns](examples/dotnet-winforms/README.md#ccli-wrapper-patterns-for-net-integration) - gcroot, Finalizer, LockBits +3. [Production Guidelines](examples/dotnet-winforms/README.md#production-quality-review) - IDisposable, thread safety + +### I want to build a WPF (.NET) app +1. [WPF Integration](examples/dotnet-winforms/README.md#option-3-wpf-c--ccli-wrapper) - C++/CLI + BitmapSource +2. [Bitmap Conversion](examples/dotnet-winforms/README.md#2-bitmap--bitmapsource-conversion) - Freeze(), Dispatcher +3. [Production Guidelines](examples/dotnet-winforms/README.md#production-quality-review) - Performance optimization + +### I want to use C# / .NET Framework (general) +1. [.NET Integration Overview](examples/dotnet-winforms/README.md) - **Complete C++/CLI wrapper guide** +2. [Raw Video Capture](examples/raw-video-capture.md) - YUV→RGB conversion patterns +3. [Session Join Pattern](examples/session-join-pattern.md) - SDK initialization flow + +### I want to wrap ANY native C++ library for .NET +1. [C++/CLI Wrapper Patterns](examples/dotnet-winforms/README.md#ccli-wrapper-patterns-for-net-integration) - **Complete 8-pattern guide** +2. [Pattern 1: Basic Structure](examples/dotnet-winforms/README.md#pattern-1-basic-wrapper-structure) - Project setup + class layout +3. [Pattern 3: gcroot Callbacks](examples/dotnet-winforms/README.md#pattern-3-gcrootT-for-nativemanaged-callbacks) - Native→Managed events +4. [Pattern 4: IDisposable](examples/dotnet-winforms/README.md#pattern-4-destructor--finalizer-idisposable) - Cleanup pattern +5. [Common Errors](examples/dotnet-winforms/README.md#common-wrapper-errors) - Troubleshooting + +### I want to implement a specific feature +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - **START HERE!** +2. [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigate to the feature +3. [API Reference](references/windows-reference.md) - Method signatures + +--- + +## Most Critical Documents + +### 1. SDK Architecture Pattern (MASTER DOCUMENT) +**[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** + +The universal 3-step pattern: +1. Get singleton (SDK, helpers, session, users) +2. Implement delegate (event callbacks) +3. Subscribe and use + +### 2. Windows Message Loop (MOST COMMON ISSUE) +**[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** + +99% of "callbacks not firing" issues are caused by missing Windows message loop. + +### 3. Singleton Hierarchy (NAVIGATION MAP) +**[concepts/singleton-hierarchy.md](concepts/singleton-hierarchy.md)** + +5-level deep navigation showing how to reach every feature. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Windows Message Loop is MANDATORY** + - SDK uses Windows message pump for callbacks + - Without it, callbacks are queued but never fire + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Subscribe in onUserVideoStatusChanged, NOT onUserJoin** + - Video may not be ready when user joins + - Wait for video status change callback + - See: [Video Rendering](examples/video-rendering.md) + +3. **Two Rendering Paths** + - Canvas API: SDK renders to your HWND (recommended) + - Raw Data Pipe: You receive YUV frames (advanced) + - See: [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) + +4. **Helpers Control YOUR Streams Only** + - `videoHelper->startVideo()` starts YOUR camera + - To see others, subscribe to their Canvas/Pipe + - See: [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +5. **UI Framework Integration Differs by Platform** + - **Win32**: Direct SDK, Canvas API (SDK renders to HWND) - best performance + - **WinForms**: C++/CLI wrapper, Raw Data Pipe, YUV→Bitmap, InvokeRequired + - **WPF**: Same wrapper + Bitmap→BitmapSource, Dispatcher, Freeze() + - See: [UI Framework Integration](examples/dotnet-winforms/README.md) + +6. **C++/CLI Wrapper Patterns (for ANY native library → .NET)** + - `void*` pointers - hide native types from managed headers + - `gcroot` - prevent GC from collecting managed references in native code + - Finalizer + Destructor - `~Class()` and `!Class()` for IDisposable cleanup + - `pin_ptr` + `Marshal::Copy` - array/buffer conversion + - `LockBits` - 100x faster than SetPixel for image manipulation + - Thread marshaling - InvokeRequired (WinForms) / Dispatcher (WPF) + - See: [C++/CLI Wrapper Guide](examples/dotnet-winforms/README.md#ccli-wrapper-patterns-for-net-integration) + +7. **Audio Connection Timing** + - Set `audioOption.connect = false` during join + - Call `startAudio()` in `onSessionJoin` callback + - See: [Production Guidelines](examples/dotnet-winforms/README.md#production-quality-review) + +--- + +## Quick Reference + +### "My code won't compile" +→ [Build Errors Guide](troubleshooting/build-errors.md) + +### "Callbacks never fire" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Video subscription returns error 2" +→ [Video Rendering](examples/video-rendering.md) - Subscribe in onUserVideoStatusChanged + +### "Abstract class error" +→ [Delegate Methods](references/delegate-methods.md) + +### "How do I implement [feature]?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I navigate to [controller]?" +→ [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +### "What error code means what?" +→ [Common Issues](troubleshooting/common-issues.md) + +--- + +## Document Version + +Based on **Zoom Video SDK for Windows v2.x** + +--- + +**Happy coding!** + +Remember: The [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) is your key to unlocking the entire SDK. Read it first! + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md new file mode 100644 index 00000000..8db88da7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md @@ -0,0 +1,327 @@ +# Canvas API vs Raw Data Pipe + +## The Two Rendering Paths + +The Zoom Video SDK provides **two distinct ways** to render video. Your choice affects quality, performance, and capabilities. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ RENDERING DECISION │ +├─────────────────────────────────────────────────────────────────┤ +│ Canvas API (SDK-Rendered) │ Raw Data Pipe (Self-Rendered) │ +│ ─────────────────────────────│──────────────────────────────── │ +│ SDK renders to your HWND │ You receive YUV420 frames │ +│ Best quality, zero effort │ Full control, more work │ +│ Standard video apps │ AI, effects, recording │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Quick Decision Guide + +| Use Case | Recommended Approach | +|----------|---------------------| +| Standard video conferencing UI | **Canvas API** | +| Video grid/gallery layout | **Canvas API** | +| Simple video display | **Canvas API** | +| Custom video effects/filters | Raw Data Pipe | +| AI/ML video processing | Raw Data Pipe | +| Custom recording format | Raw Data Pipe | +| Video compositing | Raw Data Pipe | + +**Default recommendation: Canvas API** unless you need frame-level access. + +--- + +## Canvas API (SDK-Rendered) + +### How It Works + +``` +IZoomVideoSDKUser + └── GetVideoCanvas() + └── IZoomVideoSDKCanvas + └── subscribeWithView(HWND, aspect, resolution) + └── SDK renders directly to your window + └── Hardware-accelerated, optimized +``` + +### Code Example + +```cpp +// Get user's canvas +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + +// Subscribe - SDK renders to your window +ZoomVideoSDKErrors err = canvas->subscribeWithView( + hwnd, // Your window handle + ZoomVideoSDKVideoAspect_PanAndScan, // Aspect ratio handling + ZoomVideoSDKResolution_Auto // Let SDK choose +); + +if (err == ZoomVideoSDKErrors_Success) { + // Done! SDK is now rendering to your window +} + +// To stop +canvas->unSubscribeWithView(hwnd); +``` + +### Aspect Ratio Options + +| Option | Behavior | +|--------|----------| +| `ZoomVideoSDKVideoAspect_Original` | Letterbox/pillarbox, no cropping | +| `ZoomVideoSDKVideoAspect_FullFilled` | Fill window, may crop edges | +| `ZoomVideoSDKVideoAspect_PanAndScan` | Smart crop to fill window | +| `ZoomVideoSDKVideoAspect_LetterBox` | Show full video with black bars | + +### Resolution Options + +| Option | Resolution | +|--------|------------| +| `ZoomVideoSDKResolution_90P` | 160x90 | +| `ZoomVideoSDKResolution_180P` | 320x180 | +| `ZoomVideoSDKResolution_360P` | 640x360 | +| `ZoomVideoSDKResolution_720P` | 1280x720 | +| `ZoomVideoSDKResolution_1080P` | 1920x1080 | +| `ZoomVideoSDKResolution_Auto` | SDK chooses (recommended) | + +### Advantages + +- **Best quality** - Hardware-accelerated rendering +- **No artifacts** - Professional video quality +- **Simple code** - 3 lines to subscribe +- **Better performance** - No CPU-intensive YUV conversion +- **Automatic scaling** - SDK handles window resizing +- **Aspect ratio** - Built-in handling + +### Disadvantages + +- No access to raw frames +- Can't apply custom effects +- Can't process video for AI/ML + +--- + +## Raw Data Pipe (Self-Rendered) + +### How It Works + +``` +IZoomVideoSDKUser + └── GetVideoPipe() + └── IZoomVideoSDKRawDataPipe + └── subscribe(resolution, delegate) + └── Your IZoomVideoSDKRawDataPipeDelegate + └── onRawDataFrameReceived(YUVRawDataI420*) + └── You convert YUV→RGB and render +``` + +### Code Example + +```cpp +// Implement delegate to receive frames +class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Convert YUV420 to RGB + ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height); + + // Render to window (GDI, DirectX, OpenGL, etc.) + RenderToWindow(rgbBuffer, width, height); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + if (status == RawData_On) { + std::cout << "Video started" << std::endl; + } else { + std::cout << "Video stopped" << std::endl; + } + } +}; + +// Subscribe to raw frames +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +VideoRenderer* renderer = new VideoRenderer(); +pipe->subscribe(ZoomVideoSDKResolution_720P, renderer); + +// To stop +pipe->unSubscribe(renderer); +``` + +### YUV420 to RGB Conversion + +```cpp +void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, unsigned char* rgbBuffer) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int yIndex = y * width + x; + int uvIndex = (y / 2) * (width / 2) + (x / 2); + + int Y = (unsigned char)yBuffer[yIndex]; + int U = (unsigned char)uBuffer[uvIndex]; + int V = (unsigned char)vBuffer[uvIndex]; + + // ITU-R BT.601 conversion + int C = Y - 16; + int D = U - 128; + int E = V - 128; + + int R = (298 * C + 409 * E + 128) >> 8; + int G = (298 * C - 100 * D - 208 * E + 128) >> 8; + int B = (298 * C + 516 * D + 128) >> 8; + + // Clamp to [0, 255] + R = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store as BGR (Windows format) + rgbBuffer[yIndex * 3 + 0] = (unsigned char)B; + rgbBuffer[yIndex * 3 + 1] = (unsigned char)G; + rgbBuffer[yIndex * 3 + 2] = (unsigned char)R; + } + } +} +``` + +### GDI Rendering + +```cpp +void RenderToWindow(unsigned char* rgbBuffer, int width, int height, HWND hwnd) { + HDC hdc = GetDC(hwnd); + + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; // Negative for top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 24; + bmi.bmiHeader.biCompression = BI_RGB; + + RECT rect; + GetClientRect(hwnd, &rect); + + StretchDIBits(hdc, + 0, 0, rect.right, rect.bottom, // Destination + 0, 0, width, height, // Source + rgbBuffer, &bmi, + DIB_RGB_COLORS, SRCCOPY); + + ReleaseDC(hwnd, hdc); +} +``` + +### Advantages + +- Full access to raw YUV frames +- Can apply custom video effects +- Can process for AI/ML +- Can record to custom formats +- Can composite multiple streams + +### Disadvantages + +- **CPU intensive** - YUV conversion can cause frame drops +- **Artifacts** - Manual rendering may show tearing +- **Complex** - More code to maintain +- **Performance** - Slower than Canvas API + +--- + +## Comparison Table + +| Aspect | Canvas API | Raw Data Pipe | +|--------|------------|---------------| +| **Complexity** | 3 lines of code | 50+ lines of code | +| **Performance** | Hardware-accelerated | CPU-bound | +| **Quality** | Professional | Depends on implementation | +| **Frame Access** | No | Yes (YUV420) | +| **Custom Effects** | No | Yes | +| **AI Processing** | No | Yes | +| **Recording** | No | Yes | +| **Recommended For** | Standard apps | Advanced processing | + +--- + +## Hybrid Approach + +You can use **both** simultaneously: + +```cpp +// Use Canvas for display +user->GetVideoCanvas()->subscribeWithView(displayHwnd, aspect, resolution); + +// Use Raw Data for processing (different delegate) +user->GetVideoPipe()->subscribe(ZoomVideoSDKResolution_360P, processingDelegate); +``` + +**Tip**: Use lower resolution for processing to reduce CPU load. + +--- + +## Performance Considerations + +### Canvas API +- Zero CPU overhead for rendering +- SDK handles all optimization +- Scales automatically with window resize + +### Raw Data Pipe +- YUV conversion: ~5-10ms per frame at 720p +- Memory allocation: Consider pre-allocated buffers +- Threading: Move conversion off UI thread +- Frame drops: Expect some at high resolutions + +### Optimization Tips for Raw Data + +```cpp +// Pre-allocate buffers +class OptimizedRenderer : public IZoomVideoSDKRawDataPipeDelegate { + unsigned char* rgbBuffer = nullptr; + int bufferWidth = 0; + int bufferHeight = 0; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int w = data->GetStreamWidth(); + int h = data->GetStreamHeight(); + + // Reallocate only if resolution changed + if (w != bufferWidth || h != bufferHeight) { + delete[] rgbBuffer; + rgbBuffer = new unsigned char[w * h * 3]; + bufferWidth = w; + bufferHeight = h; + } + + // Convert and render + ConvertYUV420ToRGB(..., rgbBuffer); + RenderToWindow(rgbBuffer, w, h); + } +}; +``` + +--- + +## Related Documentation + +- [Video Rendering Example](../examples/video-rendering.md) - Canvas API code +- [Raw Video Capture Example](../examples/raw-video-capture.md) - Raw Data code +- [Singleton Hierarchy](singleton-hierarchy.md) - Canvas/Pipe navigation +- [API Reference](../references/windows-reference.md) - Method details + +--- + +**TL;DR**: Use Canvas API for standard video display. Use Raw Data Pipe only when you need frame-level access for AI, effects, or custom recording. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..0e0dfb69 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md @@ -0,0 +1,298 @@ +# SDK Architecture Pattern + +## The Universal Formula + +The Zoom Video SDK follows a **perfectly consistent architecture**. Every feature works the same way: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UNIVERSAL 3-STEP PATTERN │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. GET SINGLETON → SDK, helpers, session, users │ +│ 2. IMPLEMENT DELEGATE → Event callbacks (IZoomVideoSDKDelegate)│ +│ 3. SUBSCRIBE & USE → Call methods, receive events │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Once you understand this pattern, you can implement ANY feature.** + +--- + +## Step 1: Get Singleton + +The SDK is a tree of singleton objects. You navigate to what you need: + +```cpp +// Root singleton +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + +// Level 1: Helpers (control YOUR streams) +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); +IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); +IZoomVideoSDKShareHelper* shareHelper = sdk->getShareHelper(); +IZoomVideoSDKChatHelper* chatHelper = sdk->getChatHelper(); + +// Level 2: Session +IZoomVideoSDKSession* session = sdk->getSessionInfo(); + +// Level 3: Users +IZoomVideoSDKUser* myself = session->getMyself(); +IVideoSDKVector* remoteUsers = session->getRemoteUsers(); + +// Level 4: Canvas/Pipe (per user) +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +``` + +**Key insight**: You don't construct these objects. You navigate to them. + +--- + +## Step 2: Implement Delegate + +The SDK uses **observer pattern** for events. Implement `IZoomVideoSDKDelegate`: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // Session lifecycle + void onSessionJoin() override { + std::cout << "Joined session!" << std::endl; + // Safe to start video, subscribe to users, etc. + } + + void onSessionLeave() override { + std::cout << "Left session" << std::endl; + } + + // User events + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // New users joined - but don't subscribe to video yet! + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // NOW subscribe to video - it's ready + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } + } + } + + // Chat events + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* msg) override { + std::wcout << L"Chat: " << msg->getContent() << std::endl; + } + + // Share events + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Subscribe to remote user's screen share + shareAction->subscribeWithView(shareHwnd, ZoomVideoSDKVideoAspect_Original); + } + + // ... 80+ more callbacks (implement as empty if not needed) + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override {} + void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + // etc. +}; +``` + +**Key insight**: All 80+ methods must be implemented (even if empty). + +--- + +## Step 3: Subscribe & Use + +Register your delegate and call methods: + +```cpp +// Register delegate BEFORE joining +sdk->addListener(new MyDelegate()); + +// Initialize +ZoomVideoSDKInitParams initParams; +initParams.domain = L"https://zoom.us"; +initParams.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +sdk->initialize(initParams); + +// Join session +ZoomVideoSDKSessionContext context; +context.sessionName = L"my-session"; +context.userName = L"Bot"; +context.token = L"your-jwt-token"; +context.audioOption.connect = false; // Connect audio in onSessionJoin +sdk->joinSession(context); + +// In onSessionJoin callback: +void onSessionJoin() override { + // Start audio + sdk->getAudioHelper()->startAudio(); + + // Start video + sdk->getVideoHelper()->startVideo(); + + // Subscribe to self video + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + myself->GetVideoCanvas()->subscribeWithView(selfHwnd, aspect, resolution); +} +``` + +--- + +## Pattern Applied to Every Feature + +### Audio + +```cpp +// Get singleton +IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); + +// Use +audioHelper->startAudio(); +audioHelper->muteAudio(user); +audioHelper->unmuteAudio(user); + +// Events arrive in delegate +void onUserAudioStatusChanged(...) override { } +``` + +### Video + +```cpp +// Get singleton +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + +// Use +videoHelper->startVideo(); +videoHelper->stopVideo(); +videoHelper->switchCamera(deviceId); + +// Subscribe to user's video +user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + +// Events arrive in delegate +void onUserVideoStatusChanged(...) override { } +``` + +### Chat + +```cpp +// Get singleton +IZoomVideoSDKChatHelper* chatHelper = sdk->getChatHelper(); + +// Use +chatHelper->sendChatToAll(L"Hello everyone!"); +chatHelper->sendChatToUser(user, L"Private message"); + +// Events arrive in delegate +void onChatNewMessageNotify(...) override { } +``` + +### Screen Share + +```cpp +// Get singleton +IZoomVideoSDKShareHelper* shareHelper = sdk->getShareHelper(); + +// Use (start YOUR share) +shareHelper->startShareScreen(monitorId); +shareHelper->stopShare(); + +// Subscribe to REMOTE share (in callback) +void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* shareAction) override { + shareAction->subscribeWithView(hwnd, aspect); +} +``` + +### Command Channel + +```cpp +// Get singleton +IZoomVideoSDKCmdChannel* cmdChannel = sdk->getCmdChannel(); + +// Use +cmdChannel->sendCommandToAll(L"custom-data"); +cmdChannel->sendCommand(user, L"private-data"); + +// Events arrive in delegate +void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* cmd) override { } +``` + +--- + +## Why This Pattern Works + +| Aspect | Design Choice | Benefit | +|--------|---------------|---------| +| **Singletons** | One instance per feature | No object lifecycle management | +| **Observer** | Delegate callbacks | Decoupled, event-driven code | +| **Navigation** | Tree structure | Predictable access patterns | +| **Consistency** | Same pattern everywhere | Learn once, apply everywhere | + +--- + +## Common Mistakes + +### Mistake 1: Subscribing Too Early + +```cpp +// WRONG - video not ready +void onUserJoin(...) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Error! +} + +// CORRECT - wait for video status +void onUserVideoStatusChanged(...) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); + } +} +``` + +### Mistake 2: Missing Message Loop + +```cpp +// WRONG - callbacks never fire +sdk->joinSession(context); +while (true) { Sleep(100); } // No message pump! + +// CORRECT - process Windows messages +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### Mistake 3: Using Helpers for Remote Users + +```cpp +// WRONG - helpers control YOUR streams only +sdk->getVideoHelper()->startVideo(); // Starts YOUR camera +sdk->getVideoHelper()->stopVideo(); // Stops YOUR camera + +// CORRECT - subscribe to remote users via their Canvas +remoteUser->GetVideoCanvas()->subscribeWithView(hwnd, ...); +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](singleton-hierarchy.md) - Complete navigation tree +- [Canvas vs Raw Data](canvas-vs-raw-data.md) - Choose rendering approach +- [Delegate Methods](../references/delegate-methods.md) - All 80+ callbacks +- [Session Join Pattern](../examples/session-join-pattern.md) - Working code + +--- + +**TL;DR**: Get singleton → Implement delegate → Subscribe & use. This works for every feature. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/singleton-hierarchy.md b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/singleton-hierarchy.md new file mode 100644 index 00000000..d4d82728 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/concepts/singleton-hierarchy.md @@ -0,0 +1,492 @@ +# Singleton Hierarchy: Navigation Guide + +## Overview + +The Zoom Video SDK uses a **service locator pattern** - a tree of singletons where you navigate from the root SDK object down to specific features. You don't construct objects; you traverse to them. + +``` +You want to... You navigate to... +───────────────────────────────────────────────────── +Start your camera IZoomVideoSDK → IZoomVideoSDKVideoHelper +Mute a user IZoomVideoSDK → IZoomVideoSDKAudioHelper +Subscribe to video IZoomVideoSDKUser → IZoomVideoSDKCanvas +Get raw YUV frames IZoomVideoSDKUser → IZoomVideoSDKRawDataPipe +Send chat message IZoomVideoSDK → IZoomVideoSDKChatHelper +Start screen share IZoomVideoSDK → IZoomVideoSDKShareHelper +Subscribe to remote share IZoomVideoSDKShareAction → subscribeWithView() +``` + +--- + +## Complete Hierarchy (5 Levels Deep) + +``` +Level 0: Global Factory Function +│ +└─► CreateZoomVideoSDKObj() ──────────────────────────────────► IZoomVideoSDK* + │ + ├─► Level 1: Session & Lifecycle + │ ├── initialize(params) → ZoomVideoSDKErrors + │ ├── joinSession(context) → IZoomVideoSDKSession* + │ ├── leaveSession(end) → ZoomVideoSDKErrors + │ ├── addListener(delegate) → void + │ ├── getSessionInfo() → IZoomVideoSDKSession* + │ └── isInSession() → bool + │ + ├─► Level 1: Core Helpers (Control YOUR streams) + │ ├── getVideoHelper() → IZoomVideoSDKVideoHelper* + │ │ ├── startVideo() / stopVideo() + │ │ ├── switchCamera(deviceId) + │ │ ├── getCameraList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKCameraDevice + │ │ │ ├── getDeviceId() + │ │ │ ├── getDeviceName() + │ │ │ └── isSelectedDevice() + │ │ └── startVideoCanvasPreview(hwnd, aspect, resolution) + │ │ + │ ├── getAudioHelper() → IZoomVideoSDKAudioHelper* + │ │ ├── startAudio() / stopAudio() + │ │ ├── muteAudio(user) / unmuteAudio(user) + │ │ ├── getMicList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKMicDevice + │ │ ├── getSpeakerList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKSpeakerDevice + │ │ └── selectMic() / selectSpeaker() + │ │ + │ ├── getShareHelper() → IZoomVideoSDKShareHelper* + │ │ ├── startShareScreen(monitorId) + │ │ ├── startShareView(hwnd) + │ │ ├── startShareComputerAudio() + │ │ ├── startSharingExternalSource(source) + │ │ ├── stopShare() + │ │ ├── isOtherSharing() / isSharingOut() + │ │ ├── lockShare(lock) / isShareLocked() + │ │ ├── enableMultiShare(enable) + │ │ └── getWhiteboardHelper() → IZoomVideoSDKWhiteboardHelper* + │ │ + │ ├── getChatHelper() → IZoomVideoSDKChatHelper* + │ │ ├── sendChatToAll(message) + │ │ └── sendChatToUser(user, message) + │ │ + │ ├── getUserHelper() → IZoomVideoSDKUserHelper* + │ │ ├── removeUser(user) + │ │ ├── makeHost(user) + │ │ ├── makeManager(user) + │ │ └── changeName(user, name) + │ │ + │ ├── getRecordingHelper() → IZoomVideoSDKRecordingHelper* + │ │ + │ ├── getCmdChannel() → IZoomVideoSDKCmdChannel* + │ │ ├── sendCommand(user, cmd) + │ │ └── sendCommandToAll(cmd) + │ │ + │ ├── getLiveStreamHelper() → IZoomVideoSDKLiveStreamHelper* + │ │ + │ ├── getPhoneHelper() → IZoomVideoSDKPhoneHelper* + │ │ + │ └── getLiveTranscriptionHelper() → IZoomVideoSDKLiveTranscriptionHelper* + │ + ├─► Level 1: Settings Helpers (Configure devices & behavior) + │ ├── getAudioSettingHelper() → IZoomVideoSDKAudioSettingHelper* + │ │ ├── enableAutoAdjustMicVolume() + │ │ ├── enableStereoAudio() + │ │ └── setEchoCancellationLevel() + │ │ + │ ├── GetAudioDeviceTestHelper() → IZoomVideoSDKTestAudioDeviceHelper* + │ │ ├── startMicTest() / stopMicTest() + │ │ ├── startSpeakerTest() / stopSpeakerTest() + │ │ └── playMicTest() + │ │ + │ ├── getVideoSettingHelper() → IZoomVideoSDKVideoSettingHelper* + │ │ ├── enableHDVideo() + │ │ ├── enableMirrorEffect() + │ │ └── setVideoQualityPreference() + │ │ + │ └── getShareSettingHelper() → IZoomVideoSDKShareSettingHelper* + │ ├── enableGreenBorderWhenSharing() + │ └── setShareScreenSetting() + │ + ├─► Level 1: Advanced Helpers (Special features) + │ ├── getNetworkConnectionHelper() → IZoomVideoSDKNetworkConnectionHelper* + │ │ └── getNetworkType() + │ │ + │ ├── getCRCHelper() → IZoomVideoSDKCRCHelper* + │ │ └── callCRCDevice(address, protocol) + │ │ + │ ├── getSubSessionHelper() → IZoomVideoSDKSubSessionHelper* + │ │ ├── createSubSession(name) + │ │ ├── joinSubSession(id) + │ │ ├── leaveSubSession() + │ │ └── getSubSessionList() + │ │ + │ ├── getIncomingLiveStreamHelper()→ IZoomVideoSDKIncomingLiveStreamHelper* + │ │ ├── bindIncomingLiveStream(streamKeyId) + │ │ ├── unbindIncomingLiveStream(streamKeyId) + │ │ └── startIncomingLiveStream(streamKeyId) + │ │ + │ ├── getBroadcastStreamingController() → IZoomVideoSDKBroadcastStreamingController* + │ │ ├── startBroadcast() + │ │ └── stopBroadcast() + │ │ + │ ├── getBroadcastStreamingViewer()→ IZoomVideoSDKBroadcastStreamingViewer* + │ │ └── joinBroadcast(channelId) + │ │ + │ └── getRealTimeMediaStreamsHelper() → IZoomVideoSDKRTMSHelper* (RTMS) + │ ├── startRealTimeMediaStream() + │ └── stopRealTimeMediaStream() + │ + └─► Level 1: Session Object + │ + └── getSessionInfo() → IZoomVideoSDKSession* + ├── getSessionName() + ├── getSessionID() + ├── getSessionHost() → IZoomVideoSDKUser* + ├── getMyself() → IZoomVideoSDKUser* + │ │ + │ └─► Level 3: IZoomVideoSDKUser (LOCAL - yourself) + │ ├── getUserID() / getUserName() + │ ├── isHost() / isManager() + │ ├── getVideoStatus() → ZoomVideoSDKVideoStatus + │ ├── getAudioStatus() → ZoomVideoSDKAudioStatus + │ │ + │ ├── GetVideoCanvas() → IZoomVideoSDKCanvas* [SDK RENDERING] + │ │ └─► Level 4: Canvas API + │ │ ├── subscribeWithView(hwnd, aspect, resolution) + │ │ ├── unSubscribeWithView(hwnd) + │ │ ├── setAspectMode(aspect) + │ │ └── setResolution(resolution) + │ │ + │ ├── GetVideoPipe() → IZoomVideoSDKRawDataPipe* [RAW DATA] + │ │ └─► Level 4: Raw Data Pipe + │ │ ├── subscribe(resolution, delegate) + │ │ ├── unSubscribe(delegate) + │ │ ├── getVideoStatus() + │ │ │ + │ │ └─► Level 5: IZoomVideoSDKRawDataPipeDelegate (your callback) + │ │ ├── onRawDataFrameReceived(YUVRawDataI420*) + │ │ └── onRawDataStatusChanged(status) + │ │ + │ ├── getShareActionList() → IVideoSDKVector* + │ │ └─► Level 4: IZoomVideoSDKShareAction (for share subscription) + │ │ ├── getShareCanvas() → IZoomVideoSDKCanvas* + │ │ ├── getSharePipe() → IZoomVideoSDKRawDataPipe* + │ │ ├── getShareStatus() → ZoomVideoSDKShareStatus + │ │ ├── getShareType() → ZoomVideoSDKShareType + │ │ ├── getShareSourceId() + │ │ ├── isAnnotationPrivilegeEnabled() + │ │ └── getRemoteControlHelper() → IZoomVideoSDKRemoteControlHelper* (Win/Mac) + │ │ + │ └── getRemoteCameraControlHelper() → IZoomVideoSDKRemoteCameraControlHelper* + │ + └── getRemoteUsers() → IVideoSDKVector* + │ + └─► Level 3: IZoomVideoSDKUser (REMOTE - other participants) + ├── [Same methods as local user] + ├── GetVideoCanvas() → Subscribe to their video + └── GetVideoPipe() → Get their raw frames + + ┌─────────────────────────────────────────────────────────────────────────┐ + │ CALLBACK PATH (from IZoomVideoSDKDelegate) │ + │ │ + │ ⚠️ CRITICAL: Share subscription uses IZoomVideoSDKShareAction from │ + │ callback, NOT user->GetShareCanvas()! │ + │ │ + │ onUserShareStatusChanged(pShareHelper, pUser, pShareAction) │ + │ │ │ + │ └─► IZoomVideoSDKShareAction* (received in callback) │ + │ ├── getShareCanvas() → IZoomVideoSDKCanvas* │ + │ │ └── subscribeWithView(hwnd, aspect) │ + │ │ └── unSubscribeWithView(hwnd) │ + │ ├── getSharePipe() → IZoomVideoSDKRawDataPipe* │ + │ │ └── subscribe(resolution, delegate) │ + │ │ └── unSubscribe(delegate) │ + │ ├── getShareStatus() → ZoomVideoSDKShareStatus │ + │ ├── getShareType() → ZoomVideoSDKShareType │ + │ ├── getShareSourceId() │ + │ ├── getShareSourceContentSize() → ZoomVideoSDKViewSize │ + │ ├── isAnnotationPrivilegeEnabled() │ + │ └── getRemoteControlHelper() → IZoomVideoSDKRemoteControlHelper│ + │ │ + │ Pattern: Subscribe to share in onUserShareStatusChanged callback │ + │ void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* action) { │ + │ if (action->getShareStatus() == ZoomVideoSDKShareStatus_Start) { │ + │ action->getShareCanvas()->subscribeWithView(hwnd, aspect); │ + │ } │ + │ } │ + └─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Difference from Meeting SDK + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| **Root Object** | `IMeetingService` | `IZoomVideoSDK` | +| **Feature Access** | Controllers (`GetMeetingAudioController()`) | Helpers (`getAudioHelper()`) | +| **Video Subscription** | Per-user renderers | Per-user Canvas/Pipe | +| **Share Subscription** | Via callback's `IZoomVideoSDKShareAction` | Via callback's `IZoomVideoSDKShareAction` | +| **Depth** | 4 levels max | 5 levels max | + +--- + +## When to Use Each Level + +| Level | When | Example | +|-------|------|---------| +| **Level 1** | After SDK init, control YOUR streams | `sdk->getVideoHelper()->startVideo()` | +| **Level 2** | After session join, get session info | `sdk->getSessionInfo()->getMyself()` | +| **Level 3** | Get user objects | `session->getRemoteUsers()` | +| **Level 4** | Subscribe to video/share | `user->GetVideoCanvas()->subscribeWithView()` | +| **Level 5** | Receive raw frames | `pipe->subscribe(res, myDelegate)` | + +--- + +## Two Rendering Paths + +The SDK provides **two distinct paths** for video rendering: + +### Path A: Canvas API (SDK-Rendered) + +``` +IZoomVideoSDKUser + └── GetVideoCanvas() + └── IZoomVideoSDKCanvas + └── subscribeWithView(HWND, aspect, resolution) + └── SDK renders directly to your window +``` + +**Pros**: Best quality, no CPU overhead, automatic scaling +**Use for**: Standard video conferencing UI + +### Path B: Raw Data Pipe (Self-Rendered) + +``` +IZoomVideoSDKUser + └── GetVideoPipe() + └── IZoomVideoSDKRawDataPipe + └── subscribe(resolution, delegate) + └── Your IZoomVideoSDKRawDataPipeDelegate + └── onRawDataFrameReceived(YUVRawDataI420*) + └── You convert YUV→RGB and render +``` + +**Pros**: Full control over frames, can process/filter/record +**Use for**: Custom effects, AI processing, recording + +--- + +## Universal Pattern (3 Steps) + +Every feature follows the **same pattern**: + +```cpp +// Step 1: Navigate to the helper (singleton) +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + +// Step 2: Use it +videoHelper->startVideo(); + +// For subscriptions, get user first: +IZoomVideoSDKUser* user = sdk->getSessionInfo()->getMyself(); +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +canvas->subscribeWithView(hwnd, aspect, resolution); +``` + +For **event-driven features**, implement `IZoomVideoSDKDelegate`: + +```cpp +// Step 1: Implement delegate +class MyDelegate : public IZoomVideoSDKDelegate { + void onUserVideoStatusChanged(...) override { + // React to video status changes + } + // ... all 80+ callbacks +}; + +// Step 2: Register +sdk->addListener(new MyDelegate()); + +// Step 3: Events arrive automatically +``` + +--- + +## Navigation by Feature + +| Feature | Navigation Path | +|---------|-----------------| +| **Start camera** | `sdk->getVideoHelper()->startVideo()` | +| **Stop camera** | `sdk->getVideoHelper()->stopVideo()` | +| **Switch camera** | `sdk->getVideoHelper()->switchCamera(deviceId)` | +| **Camera list** | `sdk->getVideoHelper()->getCameraList()` | +| **Mute audio** | `sdk->getAudioHelper()->muteAudio(user)` | +| **Unmute audio** | `sdk->getAudioHelper()->unmuteAudio(user)` | +| **Start audio** | `sdk->getAudioHelper()->startAudio()` | +| **Mic list** | `sdk->getAudioHelper()->getMicList()` | +| **Speaker list** | `sdk->getAudioHelper()->getSpeakerList()` | +| **Test mic** | `sdk->GetAudioDeviceTestHelper()->startMicTest()` | +| **Test speaker** | `sdk->GetAudioDeviceTestHelper()->startSpeakerTest()` | +| **Send chat** | `sdk->getChatHelper()->sendChatToAll(msg)` | +| **Start share** | `sdk->getShareHelper()->startShareScreen(monitorId)` | +| **Stop share** | `sdk->getShareHelper()->stopShare()` | +| **Subscribe video** | `user->GetVideoCanvas()->subscribeWithView(hwnd, ...)` | +| **Get raw frames** | `user->GetVideoPipe()->subscribe(res, delegate)` | +| **Subscribe share** | `shareAction->getShareCanvas()->subscribeWithView(hwnd, aspect)` ⚠️ From callback! | +| **Get raw share** | `shareAction->getSharePipe()->subscribe(res, delegate)` ⚠️ From callback! | +| **Kick user** | `sdk->getUserHelper()->removeUser(user)` | +| **Make host** | `sdk->getUserHelper()->makeHost(user)` | +| **Send command** | `sdk->getCmdChannel()->sendCommandToAll(cmd)` | +| **Get myself** | `sdk->getSessionInfo()->getMyself()` | +| **Get remote users** | `sdk->getSessionInfo()->getRemoteUsers()` | +| **Join subsession** | `sdk->getSubSessionHelper()->joinSubSession(id)` | +| **Start broadcast** | `sdk->getBroadcastStreamingController()->startBroadcast()` | +| **Transcription** | `sdk->getLiveTranscriptionHelper()->startLiveTranscription()` | + +--- + +## Critical Timing Rules + +### 1. Helpers Control YOUR Streams Only + +```cpp +// videoHelper controls YOUR camera, not others' +sdk->getVideoHelper()->startVideo(); // Starts YOUR camera +sdk->getVideoHelper()->stopVideo(); // Stops YOUR camera + +// To SEE other users' video, subscribe via their Canvas/Pipe +IZoomVideoSDKUser* remoteUser = ...; +remoteUser->GetVideoCanvas()->subscribeWithView(hwnd, ...); +``` + +### 2. Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +```cpp +// WRONG - user's video may not be ready yet +void onUserJoin(..., userList) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Error 2! +} + +// CORRECT - wait for video status change +void onUserVideoStatusChanged(..., userList) { + for (auto user : userList) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Works! + } + } +} +``` + +### 3. ShareAction Comes from Callback (CRITICAL!) + +⚠️ **Share subscription is DIFFERENT from video subscription!** + +```cpp +// WRONG - Don't use user->GetShareCanvas() for remote share! +user->GetShareCanvas()->subscribeWithView(hwnd, ...); // Won't work! + +// CORRECT - Use IZoomVideoSDKShareAction from the callback +void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) { + // Canvas API (SDK-rendered) + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->subscribeWithView(shareHwnd, ZoomVideoSDKVideoAspect_Original); + } + + // OR Raw Data Pipe (self-rendered) + // IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + // sharePipe->subscribe(ZoomVideoSDKResolution_720P, myDelegate); + } + else if (status == ZoomVideoSDKShareStatus_Stop) { + // Unsubscribe when share stops + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareHwnd); + } + } +} +``` + +**Why?** The `IZoomVideoSDKShareAction` represents a specific share stream and is only valid within the callback context. You cannot navigate to it via user objects. + +### 4. Check nullptr Before Use + +```cpp +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +if (canvas) { + canvas->subscribeWithView(hwnd, aspect, resolution); +} +``` + +--- + +## Practical Rules + +### 1. Get Helpers After Initialize + +```cpp +// WRONG - SDK not initialized +IZoomVideoSDKVideoHelper* helper = sdk->getVideoHelper(); // nullptr! +sdk->initialize(params); + +// CORRECT +sdk->initialize(params); +IZoomVideoSDKVideoHelper* helper = sdk->getVideoHelper(); // Valid +``` + +### 2. Get Session/Users After Join + +```cpp +// WRONG - not in session yet +IZoomVideoSDKSession* session = sdk->getSessionInfo(); // nullptr! +sdk->joinSession(context); + +// CORRECT - wait for onSessionJoin callback +void onSessionJoin() { + IZoomVideoSDKSession* session = sdk->getSessionInfo(); // Valid + IZoomVideoSDKUser* myself = session->getMyself(); // Valid +} +``` + +### 3. One HWND Per Video Stream + +```cpp +// Each user needs their own window +HWND selfWindow = CreateWindow(...); +HWND user1Window = CreateWindow(...); +HWND user2Window = CreateWindow(...); + +myself->GetVideoCanvas()->subscribeWithView(selfWindow, ...); +user1->GetVideoCanvas()->subscribeWithView(user1Window, ...); +user2->GetVideoCanvas()->subscribeWithView(user2Window, ...); +``` + +--- + +## Deepest Paths (Maximum Depth = 5) + +| Path | Use Case | +|------|----------| +| `IZoomVideoSDK` → `getVideoHelper()` → `getCameraList()` → `IZoomVideoSDKCameraDevice` → `getDeviceId()` | Enumerate cameras | +| `IZoomVideoSDK` → `getSessionInfo()` → `getMyself()` → `GetVideoPipe()` → `subscribe(delegate)` | Raw self video | +| `IZoomVideoSDK` → `getSessionInfo()` → `getRemoteUsers()` → `user->GetVideoCanvas()` → `subscribeWithView()` | Remote video display | + +--- + +## Related Documentation + +- [API Reference](../references/windows-reference.md) - Complete method signatures +- [SKILL.md](../SKILL.md) - Main skill overview with code examples +- [Video Rendering Guide](../SKILL.md#video-rendering---two-approaches) - Canvas vs Raw Data comparison + +--- + +**TL;DR**: Start at `IZoomVideoSDK`, navigate to helpers for YOUR streams, navigate to users for THEIR streams. Subscribe to video in `onUserVideoStatusChanged`, not `onUserJoin`. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/cloud-recording.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/cloud-recording.md new file mode 100644 index 00000000..93360268 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/cloud-recording.md @@ -0,0 +1,317 @@ +# Cloud Recording + +Complete working code for controlling cloud recording in sessions. + +**Official Sample**: `VSDK_CloudRecording` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Cloud recording saves session recordings to Zoom's cloud storage. Features: +- Start/stop recording programmatically +- Recording consent handling +- Recording status notifications + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CLOUD RECORDING FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Check canStartRecording() │ +│ 2. Start recording → startCloudRecording() │ +│ 3. Handle consent → onCloudRecordingStatus() callback │ +│ 4. Stop recording → stopCloudRecording() │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +- Session must have cloud recording enabled +- User must have recording privileges (host or granted permission) +- Valid Zoom account with cloud recording quota + +--- + +## Complete Working Code + +### RecordingManager.h + +```cpp +#pragma once +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class RecordingManager { +public: + RecordingManager(IZoomVideoSDK* sdk); + + // Recording control + bool StartRecording(); + bool StopRecording(); + bool PauseRecording(); + bool ResumeRecording(); + + // Status + bool CanStartRecording(); + bool IsRecording() const { return m_isRecording; } + + // Called from delegate + void OnRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler); + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKRecordingHelper* m_recordingHelper; + bool m_isRecording; +}; +``` + +### RecordingManager.cpp + +```cpp +#include "RecordingManager.h" +#include + +RecordingManager::RecordingManager(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_recordingHelper(nullptr) + , m_isRecording(false) { +} + +bool RecordingManager::CanStartRecording() { + m_recordingHelper = m_sdk->getRecordingHelper(); + if (!m_recordingHelper) { + std::cout << "Recording helper not available" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->canStartRecording(); + if (err == ZoomVideoSDKErrors_Success) { + return true; + } + + std::cout << "Cannot start recording: " << err << std::endl; + return false; +} + +bool RecordingManager::StartRecording() { + if (!CanStartRecording()) { + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->startCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Cloud recording started" << std::endl; + return true; + } + + std::cout << "Start recording failed: " << err << std::endl; + return false; +} + +bool RecordingManager::StopRecording() { + if (!m_recordingHelper) { + m_recordingHelper = m_sdk->getRecordingHelper(); + } + + if (!m_recordingHelper) { + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->stopCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Cloud recording stopped" << std::endl; + m_isRecording = false; + return true; + } + + std::cout << "Stop recording failed: " << err << std::endl; + return false; +} + +bool RecordingManager::PauseRecording() { + if (!m_recordingHelper) return false; + + ZoomVideoSDKErrors err = m_recordingHelper->pauseCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Recording paused" << std::endl; + return true; + } + return false; +} + +bool RecordingManager::ResumeRecording() { + if (!m_recordingHelper) return false; + + ZoomVideoSDKErrors err = m_recordingHelper->resumeCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Recording resumed" << std::endl; + return true; + } + return false; +} + +void RecordingManager::OnRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) { + switch (status) { + case RecordingStatus_Start: + std::cout << "Recording started" << std::endl; + m_isRecording = true; + break; + + case RecordingStatus_Stop: + std::cout << "Recording stopped" << std::endl; + m_isRecording = false; + break; + + case RecordingStatus_Pause: + std::cout << "Recording paused" << std::endl; + break; + + case RecordingStatus_Connecting: + std::cout << "Recording connecting..." << std::endl; + break; + + case RecordingStatus_DiskFull: + std::cout << "Recording stopped - disk full!" << std::endl; + m_isRecording = false; + break; + + default: + std::cout << "Recording status: " << status << std::endl; + } + + // Handle consent if required + if (handler) { + // Automatically accept recording consent + // In production, you may want to prompt the user + handler->accept(); + std::cout << "Recording consent accepted" << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + RecordingManager* m_recordingManager; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_recordingManager = new RecordingManager(sdk); + } + + void onSessionJoin() override { + // Start recording when session begins + if (m_recordingManager->CanStartRecording()) { + m_recordingManager->StartRecording(); + } + } + + void onSessionLeave() override { + // Stop recording before leaving + if (m_recordingManager->IsRecording()) { + m_recordingManager->StopRecording(); + } + } + + void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + m_recordingManager->OnRecordingStatus(status, handler); + } + + void onUserRecordingConsent(IZoomVideoSDKUser* user) override { + std::wcout << L"User gave recording consent: " + << user->getUserName() << std::endl; + } + + // ... other callbacks +}; +``` + +--- + +## Recording Status Values + +| Status | Description | +|--------|-------------| +| `RecordingStatus_Start` | Recording has started | +| `RecordingStatus_Stop` | Recording has stopped | +| `RecordingStatus_Pause` | Recording is paused | +| `RecordingStatus_Connecting` | Connecting to recording service | +| `RecordingStatus_DiskFull` | Recording stopped due to storage full | + +--- + +## Recording Consent + +When recording starts, participants may need to consent: + +```cpp +void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + if (handler) { + // Options: + handler->accept(); // Accept recording + handler->decline(); // Decline (will leave session) + } +} +``` + +--- + +## IZoomVideoSDKRecordingHelper Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `canStartRecording()` | `ZoomVideoSDKErrors` | Check if can start | +| `startCloudRecording()` | `ZoomVideoSDKErrors` | Start recording | +| `stopCloudRecording()` | `ZoomVideoSDKErrors` | Stop recording | +| `pauseCloudRecording()` | `ZoomVideoSDKErrors` | Pause recording | +| `resumeCloudRecording()` | `ZoomVideoSDKErrors` | Resume recording | +| `getCloudRecordingStatus()` | `RecordingStatus` | Get current status | + +--- + +## Common Issues + +### canStartRecording() Returns Error + +**Causes**: +- Not host or no recording permission +- Cloud recording not enabled for account +- Already recording + +**Fix**: Check permissions and account settings + +### Recording Doesn't Start + +**Cause**: Session not fully joined + +**Fix**: Wait for `onSessionJoin` before starting: +```cpp +void onSessionJoin() override { + // Safe to start recording now + recordingManager->StartRecording(); +} +``` + +### Consent Handler is NULL + +**Cause**: Consent not required for this session + +**Fix**: This is normal - not all sessions require consent + +--- + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Recording callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/command-channel.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/command-channel.md new file mode 100644 index 00000000..fb1195bb --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/command-channel.md @@ -0,0 +1,330 @@ +# Command Channel + +Complete working code for custom command messaging between participants. + +**Official Sample**: `VSDK_CommandChannel` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +The command channel enables custom data exchange between participants. Use cases: +- Application-specific signaling +- Game state synchronization +- Custom control messages +- Real-time collaboration data + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ COMMAND CHANNEL FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Sender: │ +│ getCmdChannel() → sendCommand() or sendCommandToAll() │ +│ │ +│ Receiver: │ +│ onCommandReceived(sender, command) callback │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Limitations + +| Limit | Value | +|-------|-------| +| Max message rate | 60 messages/second | +| Max message size | ~1KB recommended | +| Reliability | Best effort (not guaranteed) | + +**Note**: Commands are not persisted - late joiners won't receive previous commands. + +--- + +## Complete Working Code + +### CommandHandler.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class CommandHandler { +public: + CommandHandler(IZoomVideoSDK* sdk); + + // Send commands + bool SendToAll(const std::wstring& command); + bool SendToUser(IZoomVideoSDKUser* user, const std::wstring& command); + + // Connection status + bool IsConnected() const { return m_connected; } + + // Callbacks from delegate + void OnCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* command); + void OnConnectResult(bool success); + + // Set message handler + using MessageCallback = std::function; + void SetMessageHandler(MessageCallback callback) { m_callback = callback; } + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKCmdChannel* m_cmdChannel; + bool m_connected; + MessageCallback m_callback; +}; +``` + +### CommandHandler.cpp + +```cpp +#include "CommandHandler.h" +#include + +CommandHandler::CommandHandler(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_cmdChannel(nullptr) + , m_connected(false) { +} + +bool CommandHandler::SendToAll(const std::wstring& command) { + if (!m_cmdChannel) { + m_cmdChannel = m_sdk->getCmdChannel(); + } + + if (!m_cmdChannel) { + std::cout << "Command channel not available" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_cmdChannel->sendCommand(nullptr, command.c_str()); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Sent to all: " << command << std::endl; + return true; + } + + std::cout << "Send failed: " << err << std::endl; + return false; +} + +bool CommandHandler::SendToUser(IZoomVideoSDKUser* user, const std::wstring& command) { + if (!user) return false; + + if (!m_cmdChannel) { + m_cmdChannel = m_sdk->getCmdChannel(); + } + + if (!m_cmdChannel) { + return false; + } + + ZoomVideoSDKErrors err = m_cmdChannel->sendCommand(user, command.c_str()); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Sent to " << user->getUserName() + << L": " << command << std::endl; + return true; + } + + std::cout << "Send failed: " << err << std::endl; + return false; +} + +void CommandHandler::OnCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* command) { + if (!sender || !command) return; + + std::wstring cmdStr(command); + std::wcout << L"Command from " << sender->getUserName() + << L": " << cmdStr << std::endl; + + // Call user handler if set + if (m_callback) { + m_callback(sender, cmdStr); + } +} + +void CommandHandler::OnConnectResult(bool success) { + m_connected = success; + std::cout << "Command channel " << (success ? "connected" : "failed") << std::endl; +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + CommandHandler* m_cmdHandler; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_cmdHandler = new CommandHandler(sdk); + + // Set message handler + m_cmdHandler->SetMessageHandler([this](IZoomVideoSDKUser* sender, + const std::wstring& cmd) { + HandleCommand(sender, cmd); + }); + } + + void onSessionJoin() override { + // Send hello to all participants + m_cmdHandler->SendToAll(L"hello"); + } + + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) override { + m_cmdHandler->OnCommandReceived(sender, strCmd); + } + + void onCommandChannelConnectResult(bool isSuccess) override { + m_cmdHandler->OnConnectResult(isSuccess); + } + +private: + void HandleCommand(IZoomVideoSDKUser* sender, const std::wstring& cmd) { + // Parse and handle commands + if (cmd == L"ping") { + m_cmdHandler->SendToUser(sender, L"pong"); + } + else if (cmd.find(L"action:") == 0) { + // Handle action command + std::wstring action = cmd.substr(7); + ProcessAction(action); + } + } + + void ProcessAction(const std::wstring& action) { + std::wcout << L"Processing action: " << action << std::endl; + } +}; +``` + +--- + +## JSON Command Pattern + +For structured data, use JSON encoding: + +```cpp +#include + +// Send JSON command +void SendJsonCommand(CommandHandler* handler, const std::string& type, + const Json::Value& data) { + Json::Value root; + root["type"] = type; + root["data"] = data; + + Json::StreamWriterBuilder builder; + std::string jsonStr = Json::writeString(builder, root); + std::wstring wideStr(jsonStr.begin(), jsonStr.end()); + + handler->SendToAll(wideStr); +} + +// Receive and parse JSON +void HandleJsonCommand(const std::wstring& cmd) { + std::string narrowStr(cmd.begin(), cmd.end()); + + Json::Value root; + Json::CharReaderBuilder builder; + std::istringstream stream(narrowStr); + + if (Json::parseFromStream(builder, stream, &root, nullptr)) { + std::string type = root["type"].asString(); + Json::Value data = root["data"]; + + if (type == "position") { + int x = data["x"].asInt(); + int y = data["y"].asInt(); + // Handle position update + } + } +} + +// Usage +Json::Value posData; +posData["x"] = 100; +posData["y"] = 200; +SendJsonCommand(cmdHandler, "position", posData); +``` + +--- + +## IZoomVideoSDKCmdChannel Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `sendCommand(user, cmd)` | `ZoomVideoSDKErrors` | Send to specific user (NULL = all) | + +--- + +## Rate Limiting + +The command channel is limited to **60 messages/second**: + +```cpp +class RateLimitedSender { + std::chrono::steady_clock::time_point m_lastSend; + static const int MIN_INTERVAL_MS = 17; // ~60/sec + +public: + bool SendWithRateLimit(CommandHandler* handler, const std::wstring& cmd) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - m_lastSend).count(); + + if (elapsed < MIN_INTERVAL_MS) { + std::this_thread::sleep_for( + std::chrono::milliseconds(MIN_INTERVAL_MS - elapsed)); + } + + m_lastSend = std::chrono::steady_clock::now(); + return handler->SendToAll(cmd); + } +}; +``` + +--- + +## Common Issues + +### Commands Not Received + +**Cause**: Channel not connected + +**Fix**: Wait for `onCommandChannelConnectResult(true)`: +```cpp +void onCommandChannelConnectResult(bool isSuccess) override { + if (isSuccess) { + // Now safe to send commands + } +} +``` + +### Error 8 (Too Frequent) + +**Cause**: Exceeding 60 messages/second limit + +**Fix**: Add rate limiting (see above) + +### Unicode Issues + +**Cause**: Encoding mismatch + +**Fix**: Use `std::wstring` consistently: +```cpp +m_cmdChannel->sendCommand(user, L"message"); // Wide string literal +``` + +--- + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Command callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/dotnet-winforms/README.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/dotnet-winforms/README.md new file mode 100644 index 00000000..060a8a60 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/dotnet-winforms/README.md @@ -0,0 +1,1226 @@ +# UI Integration Guide for Zoom Video SDK Windows + +This guide covers three different UI approaches for integrating the Zoom Video SDK: + +1. **Win32 (Native C++)** - Direct SDK usage, no wrapper +2. **WinForms (C# .NET)** - Requires C++/CLI wrapper +3. **WPF (C# .NET)** - Requires C++/CLI wrapper + BitmapSource conversion + +## Quick Comparison + +| Aspect | Win32 | WinForms | WPF | +|--------|-------|----------|-----| +| **Language** | C++ | C# | C# | +| **Wrapper Required** | No | Yes (C++/CLI) | Yes (C++/CLI) | +| **Video Rendering** | Canvas API (SDK renders) | Raw Data Pipe (you render) | Raw Data Pipe + BitmapSource | +| **Performance** | Best | Good | Good (extra conversion) | +| **Complexity** | Medium | Medium | Higher | +| **UI Threading** | Win32 message loop | `InvokeRequired` | `Dispatcher` | + +--- + +## Option 1: Win32 (Native C++) - Direct SDK + +**No wrapper needed.** The SDK is native C++, so Win32 apps use it directly. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Win32 Dialog │────►│ Native C++ SDK │ +│ (main.cpp) │◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ + HWND Canvas API +``` + +### Key Patterns + +#### 1. SDK Manager Class (Native C++) + +```cpp +// ZoomSDKManager.h +class ZoomSDKManager { +private: + IZoomVideoSDK* m_pZoomSDK; + IZoomVideoSDKSession* m_pSession; + CustomZoomDelegate* m_pDelegate; + +public: + bool Initialize(); + bool JoinSession(const std::string& name, const std::string& token, ...); + bool StartVideo(); + bool StartVideoPreview(HWND hwnd); // Canvas API! + bool SubscribeRemoteVideo(HWND hwnd, const std::string& userId); +}; +``` + +#### 2. Delegate Implementation (All 80+ Callbacks) + +```cpp +class CustomZoomDelegate : public IZoomVideoSDKDelegate { +private: + ZoomSDKManager* m_pManager; + +public: + void onSessionJoin() override { + m_pManager->OnSessionStatusChanged(SessionStatus::InSession, "Joined"); + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // Handle video status changes + } + + // ... implement all 80+ callbacks +}; +``` + +#### 3. Video Rendering with Canvas API (SDK-Rendered) + +```cpp +// Start video preview - SDK renders directly to HWND +bool ZoomSDKManager::StartVideoPreview(HWND hwnd) { + IZoomVideoSDKVideoHelper* videoHelper = m_pZoomSDK->getVideoHelper(); + + // SDK renders directly to the window handle + ZoomVideoSDKErrors ret = videoHelper->startVideoCanvasPreview(hwnd); + return ret == ZoomVideoSDKErrors_Success; +} + +// Subscribe to remote user's video +bool ZoomSDKManager::SubscribeRemoteVideo(HWND hwnd, const std::string& userId) { + IZoomVideoSDKSession* session = m_pZoomSDK->getSessionInfo(); + IVideoSDKVector* userList = session->getRemoteUsers(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + + // SDK renders remote video directly to HWND + canvas->subscribeWithView(hwnd, ZoomVideoSDKVideoAspect_Original, ZoomVideoSDKResolution_Auto); + } + return true; +} +``` + +#### 4. Win32 Dialog with Video Panels + +```cpp +// main.cpp - Dialog procedure +INT_PTR CALLBACK MainDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { + case WM_INITDIALOG: + InitializeZoomSDK(); + PopulateDeviceLists(hDlg); + return TRUE; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDC_START_VIDEO: + g_pSDKManager->StartVideo(); + + // Get HWND of video panel control + HWND selfVideoHwnd = GetDlgItem(hDlg, IDC_SELF_VIDEO); + g_pSDKManager->StartVideoPreview(selfVideoHwnd); + + HWND remoteVideoHwnd = GetDlgItem(hDlg, IDC_REMOTE_VIDEO); + g_pSDKManager->SubscribeRemoteVideo(remoteVideoHwnd, ""); + break; + } + } + return FALSE; +} +``` + +### Win32 Flow Summary + +``` +1. CreateZoomVideoSDKObj() +2. Initialize SDK with params +3. Create & register CustomZoomDelegate +4. Join session +5. On IDC_START_VIDEO click: + - startVideo() → transmit your camera + - startVideoCanvasPreview(selfHwnd) → see yourself + - subscribeWithView(remoteHwnd) → see others +6. SDK renders directly to HWNDs +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + └── ZoomVideoSDK.Win32\ + ├── main.cpp # Win32 dialog + event handlers + ├── ZoomSDKManager.cpp # SDK wrapper class + ├── ZoomSDKManager.h # Header with delegate + └── main.rc # Dialog resources +``` + +--- + +## Option 2: WinForms (C# + C++/CLI Wrapper) + +**Requires C++/CLI bridge** because Zoom SDK is native C++. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ C# WinForms │────►│ C++/CLI Wrapper│────►│ Native C++ SDK │ +│ (MainForm.cs) │◄────│ (ZoomSDKManager)│◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + Events gcroot Callbacks + Bitmap^ YUV→RGB YUVRawDataI420 +``` + +### Key Patterns + +#### 1. C++/CLI Wrapper Class + +```cpp +// ZoomSDKManager.h (C++/CLI) +public ref class ZoomSDKManager { +private: + void* m_pVideoSDK; // Hide native types + void* m_pSessionHandler; // Native callback handler + +public: + // Managed events for C# consumption + event EventHandler^ SessionStatusChanged; + event EventHandler^ PreviewVideoReceived; + event EventHandler^ RemoteVideoReceived; + + bool Initialize(); + bool JoinSession(String^ name, String^ token, String^ user, String^ pw); + bool StartVideo(); +}; +``` + +#### 2. Native Callback → Managed Event (gcroot pattern) + +```cpp +// Native handler stores managed reference via gcroot +class VideoPreviewHandler : public IZoomVideoSDKRawDataPipeDelegate { +private: + gcroot m_managedHandler; // Prevents GC + +public: + VideoPreviewHandler(ZoomSDKManager^ handler) : m_managedHandler(handler) {} + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + ZoomSDKManager^ handler = static_cast(m_managedHandler); + if (handler && data) { + // Convert YUV to Bitmap + Bitmap^ bitmap = handler->ConvertYUVToBitmap( + data->GetYBuffer(), data->GetUBuffer(), data->GetVBuffer(), + data->GetStreamWidth(), data->GetStreamHeight(), ...); + + // Fire managed event + handler->OnPreviewVideoReceived(bitmap); + } + } +}; +``` + +#### 3. YUV→RGB Conversion (LockBits for Performance) + +```cpp +Bitmap^ ZoomSDKManager::ConvertYUVToBitmap(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, ...) { + Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb); + + // Lock for direct memory access (100x faster than SetPixel) + BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly, ...); + unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // YUV420 → RGB (ITU-R BT.601) + int Y = yBuffer[y * yStride + x]; + int U = uBuffer[(y/2) * uStride + (x/2)]; + int V = vBuffer[(y/2) * vStride + (x/2)]; + + int R = (298 * (Y-16) + 409 * (V-128) + 128) >> 8; + int G = (298 * (Y-16) - 100 * (U-128) - 208 * (V-128) + 128) >> 8; + int B = (298 * (Y-16) + 516 * (U-128) + 128) >> 8; + + // Write BGR (bitmap format) + rgbPtr[y * stride + x * 3 + 0] = (byte)B; + rgbPtr[y * stride + x * 3 + 1] = (byte)G; + rgbPtr[y * stride + x * 3 + 2] = (byte)R; + } + } + + bitmap->UnlockBits(data); + return bitmap; +} +``` + +#### 4. C# Consumer (WinForms) + +```csharp +// MainForm.cs +public partial class MainForm : Form { + private ZoomSDKInterop _zoomSDK; + + public MainForm() { + InitializeComponent(); + InitializeZoomSDK(); + } + + private void InitializeZoomSDK() { + _zoomSDK = new ZoomSDKInterop(); + + // Subscribe to events + _zoomSDK.SessionJoined += OnSessionJoined; + _zoomSDK.PreviewVideoReceived += OnPreviewVideo; + _zoomSDK.RemoteVideoReceived += OnRemoteVideo; + + _zoomSDK.Initialize(); + } + + private void OnPreviewVideo(object sender, VideoFrameEventArgs e) { + // Must marshal to UI thread + if (InvokeRequired) { + BeginInvoke(new Action(() => OnPreviewVideo(sender, e))); + return; + } + + // Display bitmap in PictureBox + _selfVideoPanel.Image?.Dispose(); + _selfVideoPanel.Image = e.Frame; + } +} +``` + +### WinForms Flow Summary + +``` +C# Layer: +1. new ZoomSDKInterop() → creates C++/CLI ZoomSDKManager +2. Subscribe to events (SessionJoined, PreviewVideoReceived, etc.) +3. _zoomSDK.Initialize() → SDK init +4. _zoomSDK.JoinSession(...) → join +5. _zoomSDK.StartVideo() → start camera + preview + +C++/CLI Layer: +1. Creates native SDK via CreateZoomVideoSDKObj() +2. Creates VideoPreviewHandler with gcroot +3. Starts Raw Data Pipe subscription +4. onRawDataFrameReceived → YUV→RGB → fires PreviewVideoReceived event + +C# Layer (UI Thread): +1. OnPreviewVideo receives Bitmap +2. Checks InvokeRequired for thread safety +3. Sets PictureBox.Image = bitmap +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + ├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge + │ ├── ZoomSDKManager.h # Managed class definition + │ └── ZoomSDKManager.cpp # Native ↔ Managed bridge + │ + └── ZoomVideoSDK.WinForms\ # C# WinForms App + ├── ZoomSDKInterop.cs # High-level C# wrapper + ├── MainForm.cs # UI + event handlers + └── Program.cs # Entry point +``` + +--- + +## Option 3: WPF (C# + C++/CLI Wrapper) + +**Same C++/CLI wrapper as WinForms**, but with additional WPF-specific handling. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ C# WPF │────►│ C# Interop │────►│ C++/CLI Wrapper│────►│ Native C++ SDK │ +│ (MainWindow) │◄────│ (ZoomSDKInterop)│◄────│ (ZoomSDKManager)│◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ + BitmapSource Bitmap→BitmapSource gcroot Callbacks + Dispatcher Conversion YUV→RGB YUVRawDataI420 +``` + +### Key Differences from WinForms + +| Aspect | WinForms | WPF | +|--------|----------|-----| +| **Video Type** | `System.Drawing.Bitmap` | `System.Windows.Media.Imaging.BitmapSource` | +| **UI Thread** | `InvokeRequired` + `BeginInvoke` | `Dispatcher.CheckAccess()` + `Dispatcher.BeginInvoke` | +| **Image Control** | `PictureBox.Image` | `Image.Source` | +| **Extra Step** | None | Bitmap → BitmapSource conversion | + +### Key Patterns + +#### 1. WPF-Specific Event Args + +```csharp +// WPF uses BitmapSource instead of Bitmap +public class VideoFrameEventArgs : EventArgs { + public BitmapSource Frame { get; set; } // WPF type + public string UserId { get; set; } +} +``` + +#### 2. Bitmap → BitmapSource Conversion + +```csharp +// ZoomSDKInterop.cs (WPF version) +private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) { + if (bitmap == null) return null; + + using (var memory = new MemoryStream()) { + bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Png); + memory.Position = 0; + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.StreamSource = memory; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + bitmapImage.Freeze(); // Make thread-safe for WPF + + return bitmapImage; + } +} + +// Event handler bridges C++/CLI Bitmap to WPF BitmapSource +_sdkManager.PreviewVideoReceived += (sender, e) => { + var wpfFrame = ConvertBitmapToBitmapSource(e.Frame); + PreviewVideoReceived?.Invoke(this, new VideoFrameEventArgs(wpfFrame, "self")); +}; +``` + +#### 3. WPF Dispatcher for UI Thread + +```csharp +// MainWindow.xaml.cs +private void OnPreviewVideoReceived(object sender, VideoFrameEventArgs e) { + // WPF uses Dispatcher instead of InvokeRequired + if (!Dispatcher.CheckAccess()) { + Dispatcher.BeginInvoke(new Action(OnPreviewVideoReceived), sender, e); + return; + } + + // Frame throttling (~30fps) + if (DateTime.Now - _lastPreviewVideoUpdate < _videoUpdateInterval) return; + _lastPreviewVideoUpdate = DateTime.Now; + + if (e.Frame != null) { + SelfVideoImage.Source = e.Frame; // WPF Image control + } +} +``` + +#### 4. Alternative: WriteableBitmap (Higher Performance) + +For better performance, you can write directly to WriteableBitmap: + +```csharp +private BitmapSource CreateErrorBitmapSource(int width, int height, string message) { + var writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgr24, null); + writeableBitmap.Lock(); + + unsafe { + byte* backBuffer = (byte*)writeableBitmap.BackBuffer; + int stride = writeableBitmap.BackBufferStride; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + byte* pixel = backBuffer + y * stride + x * 3; + pixel[0] = 0; // Blue + pixel[1] = 0; // Green + pixel[2] = 255; // Red + } + } + } + + writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + writeableBitmap.Unlock(); + writeableBitmap.Freeze(); // Thread-safe + + return writeableBitmap; +} +``` + +### WPF Flow Summary + +``` +Same as WinForms, with these differences: + +C# WPF Interop Layer: +1. Receives Bitmap from C++/CLI wrapper +2. Converts Bitmap → BitmapSource (PNG stream or WriteableBitmap) +3. Calls Freeze() to make cross-thread safe +4. Fires WPF-compatible event + +MainWindow (UI Thread): +1. OnPreviewVideoReceived receives BitmapSource +2. Checks Dispatcher.CheckAccess() for thread safety +3. Sets Image.Source = bitmapSource +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + ├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge (shared with WinForms) + │ ├── ZoomSDKManager.h + │ └── ZoomSDKManager.cpp + │ + └── ZoomVideoSDK.WPF\ # C# WPF App + ├── ZoomSDKInterop.cs # WPF-specific interop (BitmapSource) + ├── MainWindow.xaml # XAML layout + ├── MainWindow.xaml.cs # Code-behind with Dispatcher + └── App.xaml # Application entry +``` + +--- + +## Decision Matrix + +| If you need... | Use | Why | +|----------------|-----|-----| +| **Best performance** | Win32 | Canvas API, SDK renders directly | +| **C++ codebase** | Win32 | No interop overhead | +| **Existing WinForms app** | WinForms + C++/CLI | Natural integration | +| **Modern .NET UI** | WPF + C++/CLI | XAML, data binding | +| **Cross-platform .NET** | Consider Avalonia | WPF-like but cross-platform | + +## C++/CLI Wrapper Patterns (For .NET Integration) + +This section teaches **general C++/CLI wrapping patterns** applicable to ANY native C++ library. + +### When to Use C++/CLI + +| Scenario | Solution | +|----------|----------| +| Native C++ library → C# app | C++/CLI wrapper (this guide) | +| C library → C# app | P/Invoke (simpler, no wrapper needed) | +| COM library → C# app | COM Interop | +| .NET library → C++ app | Reverse P/Invoke or COM | + +### Project Setup + +1. **Create C++/CLI Class Library**: + - Visual Studio → New Project → "CLR Class Library (.NET Framework)" + - Or add `/clr` to existing C++ project + +2. **Project Properties**: + ``` + Configuration Properties → General: + - Common Language Runtime Support: /clr + - .NET Target Framework: v4.8 + + C/C++ → General: + - Additional Include Directories: path\to\native\sdk\include + + Linker → General: + - Additional Library Directories: path\to\native\sdk\lib + + Linker → Input: + - Additional Dependencies: native_sdk.lib + ``` + +3. **File Structure**: + ``` + MyWrapper/ + ├── MyWrapper.h # Managed ref class definition + ├── MyWrapper.cpp # Implementation + ├── NativeCallbacks.h # Native callback classes with gcroot + └── Stdafx.h # Precompiled header + ``` + +--- + +### Pattern 1: Basic Wrapper Structure + +**Goal**: Expose native C++ class to C# + +```cpp +// MyWrapper.h (C++/CLI) +#pragma once +#include // For string conversion + +using namespace System; +using namespace System::Runtime::InteropServices; + +namespace MyLibraryWrapper { + + // Forward declare native types (hide from C#) + class NativeClass; // Don't #include native headers here! + + public ref class ManagedWrapper { + private: + NativeClass* m_pNative; // Raw pointer to native object + bool m_disposed; + + public: + ManagedWrapper(); + ~ManagedWrapper(); // Destructor (IDisposable.Dispose) + !ManagedWrapper(); // Finalizer (destructor fallback) + + // Managed methods that wrap native calls + bool Initialize(); + void DoSomething(String^ param); + String^ GetResult(); + }; +} +``` + +```cpp +// MyWrapper.cpp +#include "stdafx.h" +#include "MyWrapper.h" +#include "native_sdk.h" // Include native headers in .cpp only! + +namespace MyLibraryWrapper { + + ManagedWrapper::ManagedWrapper() : m_pNative(nullptr), m_disposed(false) { + m_pNative = new NativeClass(); + } + + ManagedWrapper::~ManagedWrapper() { + this->!ManagedWrapper(); // Call finalizer + m_disposed = true; + } + + ManagedWrapper::!ManagedWrapper() { + if (m_pNative) { + delete m_pNative; + m_pNative = nullptr; + } + } + + bool ManagedWrapper::Initialize() { + if (!m_pNative) return false; + return m_pNative->init() == 0; // Native returns 0 for success + } + + void ManagedWrapper::DoSomething(String^ param) { + if (!m_pNative) return; + + // Convert managed String^ to native std::wstring + std::wstring nativeParam = msclr::interop::marshal_as(param); + m_pNative->doSomething(nativeParam.c_str()); + } + + String^ ManagedWrapper::GetResult() { + if (!m_pNative) return nullptr; + + // Convert native wchar_t* to managed String^ + const wchar_t* result = m_pNative->getResult(); + return result ? gcnew String(result) : nullptr; + } +} +``` + +--- + +### Pattern 2: Opaque void* Pointers + +**Goal**: Hide native types from managed headers (prevents header dependency leaks) + +```cpp +// In .h file - use void* to hide native types +private: + void* m_pNativeSDK; // Actually INativeSDK* + void* m_pNativeSession; // Actually INativeSession* + +// In .cpp file - cast back to real types +bool ManagedWrapper::JoinSession() { + INativeSDK* sdk = static_cast(m_pNativeSDK); + INativeSession* session = sdk->joinSession(...); + m_pNativeSession = static_cast(session); + return session != nullptr; +} +``` + +**Why**: Native SDK headers often have complex dependencies. Using `void*` means you only need to `#include` native headers in the `.cpp` file, not the `.h` file. This prevents compile errors in consuming C# projects. + +--- + +### Pattern 3: gcroot for Native→Managed Callbacks + +**Goal**: Native code needs to call back into managed code + +```cpp +// NativeCallbacks.h +#pragma once +#include // For gcroot + +// Forward declare the managed class +namespace MyLibraryWrapper { ref class ManagedWrapper; } + +// Native class that implements SDK callback interface +class NativeEventHandler : public INativeEventListener { +private: + gcroot m_managed; // GC-safe ref + +public: + NativeEventHandler(MyLibraryWrapper::ManagedWrapper^ wrapper) + : m_managed(wrapper) {} + + // Native callback (called by SDK on background thread) + void onEvent(int eventCode, const wchar_t* message) override { + // Get managed reference (prevents GC during callback) + MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + wrapper->FireManagedEvent(eventCode, gcnew String(message)); + } + } + + void onDataReceived(const unsigned char* data, int length) override { + MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + // Copy native data to managed array + array^ managedData = gcnew array(length); + Marshal::Copy(IntPtr((void*)data), managedData, 0, length); + wrapper->FireDataEvent(managedData); + } + } +}; +``` + +```cpp +// In ManagedWrapper.h - add events +public ref class ManagedWrapper { +public: + // Managed events for C# consumption + event EventHandler^ SomethingHappened; + event EventHandler^ DataReceived; + +internal: + // Called by native callback handler + void FireManagedEvent(int code, String^ message); + void FireDataEvent(array^ data); +}; +``` + +**Critical**: `gcroot` prevents the .NET garbage collector from moving/collecting the managed object while native code holds a reference. Without it, callbacks will crash. + +--- + +### Pattern 4: Destructor + Finalizer (IDisposable) + +**Goal**: Guarantee native resource cleanup + +```cpp +public ref class ManagedWrapper { +private: + NativeClass* m_pNative; + NativeEventHandler* m_pHandler; // Must also be cleaned up + bool m_disposed; + +public: + // Destructor - called by Dispose() or 'using' statement + ~ManagedWrapper() { + if (!m_disposed) { + this->!ManagedWrapper(); // Call finalizer logic + m_disposed = true; + GC::SuppressFinalize(this); // No need for finalizer now + } + } + + // Finalizer - called by GC if Dispose wasn't called + !ManagedWrapper() { + // Clean up in reverse order of creation + if (m_pHandler) { + delete m_pHandler; + m_pHandler = nullptr; + } + if (m_pNative) { + m_pNative->shutdown(); // SDK cleanup + delete m_pNative; + m_pNative = nullptr; + } + } +}; +``` + +**C# Usage**: +```csharp +// Option 1: Explicit dispose +var wrapper = new ManagedWrapper(); +try { + wrapper.Initialize(); + // use wrapper... +} finally { + wrapper.Dispose(); // Calls ~ManagedWrapper() +} + +// Option 2: using statement (preferred) +using (var wrapper = new ManagedWrapper()) { + wrapper.Initialize(); + // use wrapper... +} // Dispose() called automatically +``` + +--- + +### Pattern 5: String Conversion + +**Goal**: Convert between managed String^ and native strings + +```cpp +#include + +// Managed String^ → Native std::wstring +void SetName(String^ name) { + std::wstring nativeName = msclr::interop::marshal_as(name); + m_pNative->setName(nativeName.c_str()); +} + +// Managed String^ → Native std::string (UTF-8) +void SetNameUtf8(String^ name) { + std::string nativeName = msclr::interop::marshal_as(name); + m_pNative->setNameUtf8(nativeName.c_str()); +} + +// Native wchar_t* → Managed String^ +String^ GetName() { + const wchar_t* name = m_pNative->getName(); + return name ? gcnew String(name) : nullptr; +} + +// Native char* (UTF-8) → Managed String^ +String^ GetNameUtf8() { + const char* name = m_pNative->getNameUtf8(); + return name ? gcnew String(name, 0, strlen(name), System::Text::Encoding::UTF8) : nullptr; +} +``` + +--- + +### Pattern 6: Array/Buffer Conversion + +**Goal**: Pass binary data between managed and native code + +```cpp +// Managed array → Native buffer +void SendData(array^ data) { + if (data == nullptr || data->Length == 0) return; + + // Pin the managed array (prevents GC from moving it) + pin_ptr pinned = &data[0]; + unsigned char* nativePtr = pinned; + + m_pNative->sendData(nativePtr, data->Length); +} +// pinned automatically unpins when out of scope + +// Native buffer → Managed array +array^ ReceiveData() { + unsigned char* buffer = nullptr; + int length = 0; + + m_pNative->receiveData(&buffer, &length); + + if (!buffer || length <= 0) return nullptr; + + array^ result = gcnew array(length); + Marshal::Copy(IntPtr(buffer), result, 0, length); + + m_pNative->freeBuffer(buffer); // SDK may require this + return result; +} +``` + +--- + +### Pattern 7: Thread Marshaling (Native Thread → UI Thread) + +**Goal**: Fire events safely when native callbacks occur on background threads + +```cpp +// In native callback handler +void NativeEventHandler::onVideoFrame(YUVData* frame) { + ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + // Convert frame to managed Bitmap (still on native thread) + Bitmap^ bitmap = wrapper->ConvertYUVToBitmap(frame); + + // Fire event - consumer must marshal to UI thread + wrapper->FireVideoFrameEvent(bitmap); + } +} +``` + +**C# Consumer (WinForms)**: +```csharp +wrapper.VideoFrameReceived += (sender, e) => { + if (InvokeRequired) { + BeginInvoke(new Action(() => pictureBox.Image = e.Frame)); + } else { + pictureBox.Image = e.Frame; + } +}; +``` + +**C# Consumer (WPF)**: +```csharp +wrapper.VideoFrameReceived += (sender, e) => { + if (!Dispatcher.CheckAccess()) { + Dispatcher.BeginInvoke(new Action(() => image.Source = e.Frame)); + } else { + image.Source = e.Frame; + } +}; +``` + +--- + +### Pattern 8: LockBits for Fast Image Manipulation + +**Goal**: Convert image formats efficiently (e.g., YUV→RGB) + +```cpp +Bitmap^ ConvertYUVToBitmap(unsigned char* yuvData, int width, int height) { + Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb); + + // Lock bitmap memory for direct access (100x faster than SetPixel) + Rectangle rect(0, 0, width, height); + BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly, + PixelFormat::Format24bppRgb); + + unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer(); + int stride = data->Stride; // May include padding! + + // YUV420 to RGB conversion + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int yIndex = y * width + x; + int uvIndex = (y / 2) * (width / 2) + (x / 2); + + int Y = yuvData[yIndex]; + int U = yuvData[width * height + uvIndex]; + int V = yuvData[width * height + (width/2)*(height/2) + uvIndex]; + + // BT.601 conversion + int C = Y - 16, D = U - 128, E = V - 128; + int R = (298*C + 409*E + 128) >> 8; + int G = (298*C - 100*D - 208*E + 128) >> 8; + int B = (298*C + 516*D + 128) >> 8; + + // Clamp and write BGR (bitmap format) + unsigned char* pixel = rgbPtr + y * stride + x * 3; + pixel[0] = (B < 0) ? 0 : (B > 255) ? 255 : B; + pixel[1] = (G < 0) ? 0 : (G > 255) ? 255 : G; + pixel[2] = (R < 0) ? 0 : (R > 255) ? 255 : R; + } + } + + bitmap->UnlockBits(data); + return bitmap; +} +``` + +--- + +### Complete Wrapper Checklist + +When wrapping ANY native C++ library: + +``` +[ ] Project: C++/CLI Class Library with /clr enabled +[ ] Headers: Native includes in .cpp only, void* in .h +[ ] Lifecycle: Destructor (~) + Finalizer (!) pattern +[ ] Callbacks: gcroot in native handler classes +[ ] Strings: marshal_as or gcnew String() +[ ] Arrays: pin_ptr for managed→native, Marshal::Copy for native→managed +[ ] Threading: Document which thread callbacks fire on +[ ] Cleanup: Delete native objects in reverse order of creation +[ ] Events: C# events for callbacks, document threading requirements +``` + +--- + +### Common Wrapper Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `LNK2020: unresolved token` | Missing native .lib | Add to Linker → Input → Additional Dependencies | +| `C3767: candidate function not accessible` | Exposing native types in public API | Use void* or wrap in managed class | +| `AccessViolationException` in callback | GC moved managed object | Use gcroot in native handler | +| `ObjectDisposedException` | Using wrapper after Dispose | Check m_disposed flag | +| `BadImageFormatException` | x86/x64 mismatch | Match Platform Target to native SDK | + +--- + +## Production Quality Review + +This section provides a detailed assessment of each sample against production standards. + +### Overall Assessment Summary + +| Sample | Quality | Issues | Production Ready | +|--------|---------|--------|------------------| +| **Win32** | Good | 4 minor | Yes (with fixes) | +| **C++/CLI Wrapper** | Good | 4 minor | Yes (with fixes) | +| **WinForms** | Good | 3 minor | Yes (with fixes) | +| **WPF** | Good | 4 minor | Yes (with fixes) | + +--- + +### Win32 Sample (ZoomVideoSDK.Win32) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| All 80+ delegate callbacks | ✅ | Lines 17-358 in ZoomSDKManager.cpp | +| Null checks on SDK pointers | ✅ | Consistent throughout | +| Exception handling | ✅ | try/catch blocks on all SDK calls | +| Cleanup sequence | ✅ | leave → removeListener → cleanup → destroy | +| Canvas API usage | ✅ | `startVideoCanvasPreview`, `subscribeWithView` | +| Device enumeration | ✅ | Proper SDK interfaces | +| UTF-8 string conversion | ✅ | WideCharToMultiByte used correctly | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | `audioOption.connect = true` during join | Line 489 | Set to `false`, call `startAudio()` in `onSessionJoin` callback | +| ⚠️ Low | Global pointer without thread safety | Line 22 main.cpp | Use mutex or make thread-local | +| ⚠️ Low | Arbitrary `Sleep(100)` after init | Lines 366-367 | Use callback-based readiness check | +| ⚠️ Low | No WM_DESTROY handler | main.cpp | Add cleanup in WM_DESTROY | + +#### Recommended Fix: Audio Connection + +```cpp +// BEFORE (current - problematic) +sessionContext.audioOption.connect = true; // May miss audio callbacks + +// AFTER (recommended) +sessionContext.audioOption.connect = false; + +// Then in onSessionJoin callback: +void CustomZoomDelegate::onSessionJoin() { + if (m_pManager) { + // Now safe to connect audio + IZoomVideoSDKAudioHelper* audioHelper = m_pManager->GetSDK()->getAudioHelper(); + audioHelper->startAudio(); + } +} +``` + +--- + +### C++/CLI Wrapper (ZoomVideoSDK.Wrapper) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| gcroot usage | ✅ | Lines 25, 40-41, 57 - prevents GC collection | +| Finalizer + Destructor pattern | ✅ | Lines 245-254 - proper C++/CLI cleanup | +| LockBits for YUV conversion | ✅ | Lines 831-880 - 100x faster than SetPixel | +| All 80+ delegate callbacks | ✅ | SimpleNativeHandler implements all | +| Error handling | ✅ | Status messages for all operations | +| Video preview cleanup | ✅ | StopVideoPreview cleans up handler | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | `audioOption.connect = true` | Line 343 | Same fix as Win32 | +| ⚠️ Medium | `unSubscribe(nullptr)` | Line 987 | Pass actual delegate used for subscription | +| ⚠️ Medium | RemoteVideoHandler memory leak | Line 947 | Store handler, delete in UnsubscribeFromUserVideo | +| ⚠️ Low | Simplified stride calculation | Lines 1162, 1212 | Use `data->GetYStride()`, `GetUStride()`, `GetVStride()` | + +#### Recommended Fix: Handler Memory Leak + +```cpp +// ZoomSDKManager.h - Add member to track handlers +private: + std::map m_remoteHandlers; + +// In SubscribeToUserVideo +RemoteVideoHandler* remoteHandler = new RemoteVideoHandler(this, userId); +m_remoteHandlers[userId] = remoteHandler; // Store for later cleanup + +// In UnsubscribeFromUserVideo +if (m_remoteHandlers.ContainsKey(userId)) { + RemoteVideoHandler* handler = m_remoteHandlers[userId]; + videoPipe->unSubscribe(handler); // Pass actual handler + delete handler; + m_remoteHandlers.Remove(userId); +} +``` + +--- + +### WinForms Sample (ZoomVideoSDK.WinForms) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| Thread marshaling | ✅ | InvokeRequired + BeginInvoke throughout | +| Frame rate throttling | ✅ | 30fps limit prevents UI overload | +| Bitmap disposal | ✅ | `.Image?.Dispose()` before assignment | +| Form closing cleanup | ✅ | LeaveSession + Cleanup in OnFormClosing | +| JWT token decoder | ✅ | Proper Base64 URL-safe handling | +| Exception handling | ✅ | All operations wrapped in try/catch | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | Missing IDisposable pattern | ZoomSDKInterop.cs | Add `~ZoomSDKInterop()` destructor | +| ⚠️ Low | Null check after Cleanup | Line 468-469 | Add `_sdkManager != null` check before events | +| ⚠️ Info | Duplicate YUV conversion | Lines 369-443 | Remove - wrapper already handles this | + +#### Recommended Fix: IDisposable + +```csharp +// ZoomSDKInterop.cs +public class ZoomSDKInterop : IDisposable +{ + private bool _disposed = false; + + ~ZoomSDKInterop() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Cleanup(); + } + _disposed = true; + } + } +} +``` + +--- + +### WPF Sample (ZoomVideoSDK.WPF) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| Dispatcher thread safety | ✅ | CheckAccess() + BeginInvoke | +| BitmapImage.Freeze() | ✅ | Cross-thread safety for WPF | +| Frame rate throttling | ✅ | 30fps limit | +| Window close cleanup | ✅ | LeaveSession + Cleanup in OnClosed | +| BitmapCacheOption.OnLoad | ✅ | Proper stream handling | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | MemoryStream PNG conversion | Lines 384-395 | Use Imaging.CreateBitmapSourceFromHBitmap for speed | +| ⚠️ Medium | Missing IDisposable | ZoomSDKInterop.cs | Same fix as WinForms | +| ⚠️ Low | Duplicate YUV conversion | Lines 406-480 | Remove - wrapper handles this | +| ⚠️ Info | WriteableBitmap error bitmap | Lines 493-527 | No text drawn - consider DrawingContext | + +#### Recommended Fix: Faster Bitmap Conversion + +```csharp +// Faster alternative to MemoryStream PNG encoding +[DllImport("gdi32.dll")] +private static extern bool DeleteObject(IntPtr hObject); + +private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) +{ + if (bitmap == null) return null; + + IntPtr hBitmap = bitmap.GetHbitmap(); + try + { + var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + source.Freeze(); + return source; + } + finally + { + DeleteObject(hBitmap); // Prevent GDI handle leak + } +} +``` + +--- + +### Cross-Sample Issues + +These issues appear in multiple samples: + +#### 1. Audio Connection Timing + +**Affected:** Win32, C++/CLI Wrapper + +```cpp +// Current (all samples) +sessionContext.audioOption.connect = true; + +// Recommended per Zoom docs +sessionContext.audioOption.connect = false; +// Then call startAudio() in onSessionJoin callback +``` + +**Why:** Setting `connect = true` during join may cause audio to connect before the session is fully established, potentially missing early audio callbacks. + +#### 2. Video Subscription Timing + +**Pattern followed correctly:** All samples wait for `onUserJoin` or `onUserVideoStatusChanged` before subscribing to video. + +#### 3. Resource Cleanup + +**Pattern followed correctly:** All samples implement cleanup in form/window closing handlers. + +--- + +### Production Checklist + +Use this checklist before deploying: + +``` +[ ] Audio: Set audioOption.connect = false, start in onSessionJoin +[ ] Video: Subscribe only after onUserJoin/onUserVideoStatusChanged +[ ] Memory: Track and cleanup all native handlers +[ ] Threading: Marshal all UI updates to main thread +[ ] Disposal: Implement IDisposable with destructor/finalizer +[ ] Frame Rate: Throttle video at 30fps to prevent UI lock +[ ] Error Handling: Try/catch all SDK calls +[ ] Cleanup: LeaveSession before Cleanup on app close +``` + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../../concepts/sdk-architecture-pattern.md) +- [Video Rendering](../video-rendering.md) +- [Screen Share Subscription](../screen-share-subscription.md) +- [Delegate Methods](../../references/delegate-methods.md) + +## Sample Locations + +All samples are in: +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ +├── ZoomVideoSDK.Win32\ # Native Win32 + Canvas API +├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge (shared) +├── ZoomVideoSDK.WinForms\ # C# WinForms + Raw Data +└── ZoomVideoSDK.WPF\ # C# WPF + BitmapSource +``` diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-audio-capture.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-audio-capture.md new file mode 100644 index 00000000..a72572cd --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-audio-capture.md @@ -0,0 +1,289 @@ +# Raw Audio Capture + +Complete working code for capturing raw PCM audio from sessions. + +**Official Sample**: `VSDK_getRawAudio` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +The SDK provides two audio capture modes: +- **Mixed Audio**: All participants combined into single stream +- **Per-User Audio**: Separate stream for each participant + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AUDIO CAPTURE FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ IZoomVideoSDKDelegate │ +│ ├── onMixedAudioRawDataReceived(AudioRawData*) [Combined] │ +│ └── onOneWayAudioRawDataReceived(AudioRawData*, user) [Per-user]│ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Audio Format + +| Property | Value | +|----------|-------| +| Format | PCM (uncompressed) | +| Sample Rate | 32000 Hz | +| Bit Depth | 16-bit signed | +| Channels | Mono (1) or Stereo (2) | +| Byte Order | Little-endian | + +**Buffer size per callback**: Varies, typically 640-1280 bytes (20-40ms of audio) + +--- + +## Complete Working Code + +### AudioCapture.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class AudioCapture { +public: + AudioCapture(const std::string& outputPath); + ~AudioCapture(); + + // Called from delegate + void OnMixedAudio(AudioRawData* data); + void OnUserAudio(AudioRawData* data, IZoomVideoSDKUser* user); + + // Statistics + int GetSampleCount() const { return m_sampleCount; } + +private: + std::ofstream m_outputFile; + std::mutex m_mutex; + int m_sampleCount; + int m_sampleRate; + int m_channels; +}; +``` + +### AudioCapture.cpp + +```cpp +#include "AudioCapture.h" +#include + +AudioCapture::AudioCapture(const std::string& outputPath) + : m_sampleCount(0) + , m_sampleRate(0) + , m_channels(0) { + + m_outputFile.open(outputPath, std::ios::binary); + if (!m_outputFile.is_open()) { + std::cerr << "Failed to open: " << outputPath << std::endl; + } +} + +AudioCapture::~AudioCapture() { + std::lock_guard lock(m_mutex); + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + std::cout << "Captured " << m_sampleCount << " audio samples" << std::endl; + std::cout << "Sample rate: " << m_sampleRate << " Hz" << std::endl; + std::cout << "Channels: " << m_channels << std::endl; +} + +void AudioCapture::OnMixedAudio(AudioRawData* data) { + if (!data) return; + + std::lock_guard lock(m_mutex); + + // Get audio properties + char* buffer = data->GetBuffer(); + unsigned int length = data->GetBufferLen(); + unsigned int sampleRate = data->GetSampleRate(); + unsigned int channels = data->GetChannelNum(); + + // Log format on first callback + if (m_sampleCount == 0) { + std::cout << "Audio format: " << sampleRate << " Hz, " + << channels << " channel(s)" << std::endl; + m_sampleRate = sampleRate; + m_channels = channels; + } + + // Write PCM data + if (m_outputFile.is_open() && buffer && length > 0) { + m_outputFile.write(buffer, length); + } + + m_sampleCount++; + + // Log progress + if (m_sampleCount % 500 == 0) { + std::cout << "Audio samples: " << m_sampleCount << std::endl; + } +} + +void AudioCapture::OnUserAudio(AudioRawData* data, IZoomVideoSDKUser* user) { + if (!data || !user) return; + + // Process per-user audio + // Could write to separate files per user + std::wcout << L"Audio from: " << user->getUserName() << std::endl; +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + AudioCapture* m_audioCapture; + +public: + MyDelegate() { + m_audioCapture = new AudioCapture("audio.pcm"); + } + + ~MyDelegate() { + delete m_audioCapture; + } + + // Mixed audio from all participants + void onMixedAudioRawDataReceived(AudioRawData* data) override { + m_audioCapture->OnMixedAudio(data); + } + + // Audio from specific user + void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + m_audioCapture->OnUserAudio(data, user); + } + + // Shared audio (from screen share with audio) + void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Handle shared audio separately if needed + } + + // ... other callbacks +}; +``` + +--- + +## Playing Raw PCM Audio + +### FFplay + +```cmd +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert to WAV + +```cmd +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate 32000 Hz | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +--- + +## AudioRawData Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `GetBuffer()` | `char*` | PCM sample buffer | +| `GetBufferLen()` | `unsigned int` | Buffer size in bytes | +| `GetSampleRate()` | `unsigned int` | Sample rate (usually 32000) | +| `GetChannelNum()` | `unsigned int` | 1 = mono, 2 = stereo | + +--- + +## Audio Callback Types + +### onMixedAudioRawDataReceived + +Combined audio from all participants: + +```cpp +void onMixedAudioRawDataReceived(AudioRawData* data) override { + // All participants mixed together + // Best for recording entire session +} +``` + +### onOneWayAudioRawDataReceived + +Per-user audio (requires special setup): + +```cpp +void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + // Audio from specific user + // Best for transcription per speaker +} +``` + +### onSharedAudioRawDataReceived + +Audio from screen share: + +```cpp +void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Audio from shared content + // Separate from participant audio +} +``` + +--- + +## Common Issues + +### No Audio Callbacks + +**Cause**: Audio not connected + +**Fix**: Connect audio in `onSessionJoin`: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); +} +``` + +### Wrong Sample Rate in FFmpeg + +**Cause**: Assuming 44100 Hz instead of 32000 Hz + +**Fix**: Use correct rate: +```cmd +ffplay -ar 32000 ... # Not 44100! +``` + +### Audio is Silent + +**Cause**: No one is speaking or all muted + +**Fix**: Check `onUserAudioStatusChanged` for mute status + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Video frame capture +- [Send Raw Audio](send-raw-audio.md) - Audio injection +- [Delegate Methods](../references/delegate-methods.md) - All callbacks +- [API Reference](../references/windows-reference.md) - AudioRawData methods diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-video-capture.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-video-capture.md new file mode 100644 index 00000000..d71c2dc8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/raw-video-capture.md @@ -0,0 +1,424 @@ +# Raw Video Capture + +Complete working code for capturing raw YUV420 video frames for custom processing. + +## Overview + +Use Raw Data Pipe when you need frame-level access for: +- AI/ML video processing +- Custom video effects +- Recording to custom formats +- Video compositing + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ RAW DATA FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ GetVideoPipe() → subscribe(resolution, delegate) │ +│ ↓ │ +│ onRawDataFrameReceived(YUVRawDataI420*) │ +│ ↓ │ +│ Process: GetYBuffer(), GetUBuffer(), GetVBuffer() │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## YUV420 Format Explained + +### Memory Layout (I420 Planar) + +``` +┌─────────────────────────────────────────┐ +│ Y Plane (Luminance/Brightness) │ width × height bytes +│ Full resolution │ +├─────────────────────────────────────────┤ +│ U Plane (Blue-difference Chrominance) │ (width/2) × (height/2) bytes +│ Quarter resolution │ +├─────────────────────────────────────────┤ +│ V Plane (Red-difference Chrominance) │ (width/2) × (height/2) bytes +│ Quarter resolution │ +└─────────────────────────────────────────┘ + +Total size = width × height × 1.5 bytes +Example: 1280×720 = 1,382,400 bytes (~1.3 MB per frame) +``` + +### Buffer Sizes + +| Resolution | Y Buffer | U Buffer | V Buffer | Total | +|------------|----------|----------|----------|-------| +| 720p (1280×720) | 921,600 | 230,400 | 230,400 | 1,382,400 | +| 1080p (1920×1080) | 2,073,600 | 518,400 | 518,400 | 3,110,400 | +| 360p (640×360) | 230,400 | 57,600 | 57,600 | 345,600 | + +--- + +## Complete Working Code + +### RawVideoCapture.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" +#include "zoom_sdk_raw_data_def.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class RawVideoCapture : public IZoomVideoSDKRawDataPipeDelegate { +public: + RawVideoCapture(const std::string& outputPath); + ~RawVideoCapture(); + + // IZoomVideoSDKRawDataPipeDelegate + void onRawDataFrameReceived(YUVRawDataI420* data) override; + void onRawDataStatusChanged(RawDataStatus status) override; + + // Statistics + int GetFrameCount() const { return m_frameCount; } + int GetWidth() const { return m_width; } + int GetHeight() const { return m_height; } + +private: + std::ofstream m_outputFile; + std::mutex m_mutex; + int m_frameCount; + int m_width; + int m_height; + std::string m_outputPath; +}; +``` + +### RawVideoCapture.cpp + +```cpp +#include "RawVideoCapture.h" +#include + +RawVideoCapture::RawVideoCapture(const std::string& outputPath) + : m_outputPath(outputPath) + , m_frameCount(0) + , m_width(0) + , m_height(0) { + + m_outputFile.open(outputPath, std::ios::binary); + if (!m_outputFile.is_open()) { + std::cerr << "Failed to open output file: " << outputPath << std::endl; + } +} + +RawVideoCapture::~RawVideoCapture() { + std::lock_guard lock(m_mutex); + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + std::cout << "Captured " << m_frameCount << " frames" << std::endl; +} + +void RawVideoCapture::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + std::lock_guard lock(m_mutex); + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Detect resolution change + if (width != m_width || height != m_height) { + std::cout << "Resolution: " << width << "x" << height << std::endl; + m_width = width; + m_height = height; + } + + // Calculate buffer sizes + size_t ySize = width * height; + size_t uvSize = ySize / 4; // Quarter resolution + + // Write YUV data to file + if (m_outputFile.is_open()) { + m_outputFile.write(data->GetYBuffer(), ySize); + m_outputFile.write(data->GetUBuffer(), uvSize); + m_outputFile.write(data->GetVBuffer(), uvSize); + } + + m_frameCount++; + + // Log progress every 100 frames + if (m_frameCount % 100 == 0) { + std::cout << "Captured " << m_frameCount << " frames" << std::endl; + } +} + +void RawVideoCapture::onRawDataStatusChanged(RawDataStatus status) { + if (status == RawData_On) { + std::cout << "Raw data started" << std::endl; + } else { + std::cout << "Raw data stopped" << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + IZoomVideoSDK* m_sdk; + std::map m_captures; + std::map m_pipes; + +public: + MyDelegate(IZoomVideoSDK* sdk) : m_sdk(sdk) {} + + ~MyDelegate() { + // Cleanup all captures + for (auto& pair : m_captures) { + delete pair.second; + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = m_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + + if (status.isOn) { + // Start capture if not already capturing + if (m_captures.find(user) == m_captures.end()) { + StartCapture(user); + } + } else { + // Stop capture + StopCapture(user); + } + } + } + + void StartCapture(IZoomVideoSDKUser* user) { + // Create unique filename + std::wstring name = user->getUserName(); + std::string filename = "video_" + + std::string(name.begin(), name.end()) + ".yuv"; + + // Create capture delegate + RawVideoCapture* capture = new RawVideoCapture(filename); + m_captures[user] = capture; + + // Subscribe to raw data + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (pipe) { + ZoomVideoSDKErrors err = pipe->subscribe( + ZoomVideoSDKResolution_720P, + capture + ); + + if (err == ZoomVideoSDKErrors_Success) { + m_pipes[user] = pipe; + std::wcout << L"Started capture for: " << user->getUserName() << std::endl; + } else { + std::wcout << L"Failed to subscribe: " << err << std::endl; + delete capture; + m_captures.erase(user); + } + } + } + + void StopCapture(IZoomVideoSDKUser* user) { + auto pipeIt = m_pipes.find(user); + auto captureIt = m_captures.find(user); + + if (pipeIt != m_pipes.end() && captureIt != m_captures.end()) { + pipeIt->second->unSubscribe(captureIt->second); + delete captureIt->second; + + m_pipes.erase(pipeIt); + m_captures.erase(captureIt); + + std::wcout << L"Stopped capture for: " << user->getUserName() << std::endl; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + StopCapture(userList->GetItem(i)); + } + } + + // ... other callbacks +}; +``` + +--- + +## Playing Raw YUV Files + +Raw YUV files have no headers - you must specify format explicitly. + +### Play with FFplay + +```cmd +ffplay -video_size 1280x720 -pixel_format yuv420p -f rawvideo video.yuv +``` + +### Convert to MP4 + +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -framerate 30 -f rawvideo -i video.yuv -c:v libx264 output.mp4 +``` + +### Key FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (must match!) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-framerate 30` | Assumed frame rate | + +--- + +## YUV to RGB Conversion + +For real-time display, convert YUV420 to RGB: + +```cpp +void ConvertYUV420ToRGB(YUVRawDataI420* data, unsigned char* rgbBuffer) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int yIndex = y * width + x; + int uvIndex = (y / 2) * (width / 2) + (x / 2); + + int Y = (unsigned char)yBuffer[yIndex]; + int U = (unsigned char)uBuffer[uvIndex]; + int V = (unsigned char)vBuffer[uvIndex]; + + // ITU-R BT.601 conversion + int C = Y - 16; + int D = U - 128; + int E = V - 128; + + int R = (298 * C + 409 * E + 128) >> 8; + int G = (298 * C - 100 * D - 208 * E + 128) >> 8; + int B = (298 * C + 516 * D + 128) >> 8; + + // Clamp to [0, 255] + R = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store as BGR (Windows format) + int rgbIndex = yIndex * 3; + rgbBuffer[rgbIndex + 0] = (unsigned char)B; + rgbBuffer[rgbIndex + 1] = (unsigned char)G; + rgbBuffer[rgbIndex + 2] = (unsigned char)R; + } + } +} +``` + +--- + +## YUVRawDataI420 Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `GetYBuffer()` | `char*` | Y plane (luminance) | +| `GetUBuffer()` | `char*` | U plane (chrominance) | +| `GetVBuffer()` | `char*` | V plane (chrominance) | +| `GetStreamWidth()` | `unsigned int` | Frame width | +| `GetStreamHeight()` | `unsigned int` | Frame height | +| `GetRotation()` | `unsigned int` | 0, 90, 180, 270 | +| `GetTimeStamp()` | `unsigned long long` | Frame timestamp | +| `GetBufferLen()` | `unsigned int` | Total buffer size | +| `CanAddRef()` | `bool` | Can add reference? | +| `AddRef()` | `bool` | Add reference | +| `Release()` | `int` | Release reference | + +--- + +## Performance Tips + +### 1. Pre-allocate Buffers + +```cpp +class OptimizedCapture : public IZoomVideoSDKRawDataPipeDelegate { + unsigned char* m_rgbBuffer = nullptr; + int m_bufferSize = 0; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int size = data->GetStreamWidth() * data->GetStreamHeight() * 3; + + // Reallocate only if needed + if (size != m_bufferSize) { + delete[] m_rgbBuffer; + m_rgbBuffer = new unsigned char[size]; + m_bufferSize = size; + } + + // Process... + } +}; +``` + +### 2. Process on Separate Thread + +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Add reference so frame survives after callback + if (data->CanAddRef()) { + data->AddRef(); + + // Queue for processing thread + m_frameQueue.push(data); + } +} + +// Processing thread +void ProcessingLoop() { + while (m_running) { + YUVRawDataI420* frame = m_frameQueue.pop(); + if (frame) { + ProcessFrame(frame); + frame->Release(); // Release when done + } + } +} +``` + +### 3. Use Lower Resolution for Processing + +```cpp +// Subscribe at lower resolution for AI processing +pipe->subscribe(ZoomVideoSDKResolution_360P, aiDelegate); + +// Subscribe at higher resolution for display (Canvas API) +user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, ZoomVideoSDKResolution_720P); +``` + +--- + +## Related Documentation + +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Choose your approach +- [Video Rendering](video-rendering.md) - Canvas API (easier) +- [API Reference](../references/windows-reference.md) - Full method signatures + +--- + +**TL;DR**: Subscribe via `GetVideoPipe()->subscribe(resolution, delegate)`, receive frames in `onRawDataFrameReceived()`, write Y/U/V buffers to file or convert to RGB. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/screen-share-subscription.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/screen-share-subscription.md new file mode 100644 index 00000000..c86cebc5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/screen-share-subscription.md @@ -0,0 +1,571 @@ +# Screen Share Subscription - Complete Guide + +## Overview + +Screen share subscription in the Zoom Video SDK is **fundamentally different** from video subscription. This guide explains why and provides complete working code for both Canvas API and Raw Data approaches. + +## Why Screen Share is Different from Video + +| Aspect | Video | Screen Share | +|--------|-------|--------------| +| **Streams per user** | One video stream | Multiple share streams possible (multi-share) | +| **Access method** | `user->GetVideoCanvas()` | `IZoomVideoSDKShareAction*` from callback | +| **Subscription timing** | `onUserVideoStatusChanged` | `onUserShareStatusChanged` | +| **Key object** | `IZoomVideoSDKUser*` | `IZoomVideoSDKShareAction*` | + +**The critical difference**: A user can have multiple active share actions simultaneously (e.g., sharing screen + sharing a whiteboard). The `IZoomVideoSDKShareAction` object in the callback represents a specific share stream. + +## The Wrong Way (Common Mistake) + +```cpp +// WRONG - This won't work for remote screen shares! +IZoomVideoSDKUser* sharingUser = ...; +IZoomVideoSDKCanvas* shareCanvas = sharingUser->GetShareCanvas(); +shareCanvas->subscribeWithView(hwnd, aspect); // May fail or show nothing! +``` + +**Why it fails**: `GetShareCanvas()` on the user object doesn't give you access to the active share stream. You MUST use the `IZoomVideoSDKShareAction*` provided in the callback. + +## The Correct Way + +### Canvas API (Recommended) + +The Canvas API lets the SDK render the shared screen directly to your window handle. + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + HWND shareWindow_; + std::map activeShares_; + +public: + MyDelegate(HWND shareWnd) : shareWindow_(shareWnd) {} + + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + ZoomVideoSDKShareType type = pShareAction->getShareType(); + + // Get user name for logging + const zchar_t* userName = pUser ? pUser->getUserName() : L"Unknown"; + + switch (status) { + case ZoomVideoSDKShareStatus_Start: + case ZoomVideoSDKShareStatus_Resume: + SubscribeToShare(pShareAction, userName); + break; + + case ZoomVideoSDKShareStatus_Pause: + // Share is paused - you may want to show a "Paused" overlay + // The subscription remains active + break; + + case ZoomVideoSDKShareStatus_Stop: + UnsubscribeFromShare(pShareAction, userName); + break; + } + } + +private: + void SubscribeToShare(IZoomVideoSDKShareAction* pShareAction, + const zchar_t* userName) + { + // Prevent duplicate subscriptions + if (activeShares_.find(pShareAction) != activeShares_.end()) { + return; + } + + // Get the share canvas from the ShareAction (NOT from the user!) + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (!shareCanvas) { + // Error: Share canvas not available + return; + } + + // Subscribe with Canvas API + ZoomVideoSDKErrors ret = shareCanvas->subscribeWithView( + shareWindow_, + ZoomVideoSDKVideoAspect_Original // Show full content, letterbox if needed + ); + + if (ret == ZoomVideoSDKErrors_Success) { + activeShares_[pShareAction] = true; + // Successfully subscribed to share from [userName] + } else { + // Failed to subscribe: error code [ret] + } + } + + void UnsubscribeFromShare(IZoomVideoSDKShareAction* pShareAction, + const zchar_t* userName) + { + auto it = activeShares_.find(pShareAction); + if (it == activeShares_.end()) { + return; // Not subscribed + } + + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareWindow_); + } + + activeShares_.erase(it); + // Unsubscribed from share + } +}; +``` + +### Raw Data Pipe (Advanced) + +Use Raw Data when you need to process the shared screen frames yourself (recording, effects, computer vision). + +```cpp +class ShareRawDataDelegate : public IZoomVideoSDKRawDataPipeDelegate { +private: + std::function frameCallback_; + +public: + ShareRawDataDelegate(std::function callback) + : frameCallback_(callback) {} + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Share frames can be large (1080p, 4K) + // Process efficiently or queue for async processing + + if (frameCallback_) { + frameCallback_(data); + } + } + + void onRawDataStatusChanged(RawDataStatus status) override { + switch (status) { + case RawData_On: + // Share raw data stream started + break; + case RawData_Off: + // Share raw data stream stopped + break; + } + } +}; + +class MyDelegate : public IZoomVideoSDKDelegate { +private: + std::map shareDataDelegates_; + +public: + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) + { + SubscribeToShareRawData(pShareAction); + } + else if (status == ZoomVideoSDKShareStatus_Stop) + { + UnsubscribeFromShareRawData(pShareAction); + } + } + +private: + void SubscribeToShareRawData(IZoomVideoSDKShareAction* pShareAction) { + if (shareDataDelegates_.find(pShareAction) != shareDataDelegates_.end()) { + return; // Already subscribed + } + + // Get the raw data pipe from ShareAction + IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + if (!sharePipe) { + return; + } + + // Create delegate to receive frames + auto* delegate = new ShareRawDataDelegate([](YUVRawDataI420* frame) { + // Process share frame here + // Example: save to file, apply effects, send to encoder + ProcessShareFrame(frame); + }); + + // Subscribe with desired resolution + ZoomVideoSDKErrors ret = sharePipe->subscribe( + ZoomVideoSDKResolution_1080P, // Request high quality for screen share + delegate + ); + + if (ret == ZoomVideoSDKErrors_Success) { + shareDataDelegates_[pShareAction] = delegate; + } else { + delete delegate; + } + } + + void UnsubscribeFromShareRawData(IZoomVideoSDKShareAction* pShareAction) { + auto it = shareDataDelegates_.find(pShareAction); + if (it == shareDataDelegates_.end()) { + return; + } + + IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + if (sharePipe) { + sharePipe->unSubscribe(it->second); + } + + delete it->second; + shareDataDelegates_.erase(it); + } + + static void ProcessShareFrame(YUVRawDataI420* frame) { + // Your frame processing logic + // Note: This runs on SDK thread - don't block! + } +}; +``` + +## Complete Working Example + +Here's a complete example showing screen share subscription with proper lifecycle management: + +```cpp +#include +#include +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class ScreenShareManager : public IZoomVideoSDKDelegate { +private: + IZoomVideoSDK* sdk_; + HWND mainShareWindow_; + + // Track active share subscriptions + struct ShareSubscription { + IZoomVideoSDKShareAction* action; + IZoomVideoSDKUser* user; + ZoomVideoSDKShareType type; + bool isSubscribed; + }; + std::vector activeSubscriptions_; + +public: + ScreenShareManager(IZoomVideoSDK* sdk, HWND shareWindow) + : sdk_(sdk), mainShareWindow_(shareWindow) {} + + // ========================================== + // IZoomVideoSDKDelegate - Share Events + // ========================================== + + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction || !pUser) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + ZoomVideoSDKShareType type = pShareAction->getShareType(); + const zchar_t* userName = pUser->getUserName(); + + // Check if this is our own share + IZoomVideoSDKSession* session = sdk_->getSessionInfo(); + IZoomVideoSDKUser* myself = session ? session->getMyself() : nullptr; + bool isMyShare = (pUser == myself); + + switch (status) { + case ZoomVideoSDKShareStatus_Start: + HandleShareStart(pShareAction, pUser, type, isMyShare); + break; + + case ZoomVideoSDKShareStatus_Resume: + HandleShareResume(pShareAction, pUser); + break; + + case ZoomVideoSDKShareStatus_Pause: + HandleSharePause(pShareAction, pUser); + break; + + case ZoomVideoSDKShareStatus_Stop: + HandleShareStop(pShareAction, pUser); + break; + } + } + +private: + void HandleShareStart(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user, + ZoomVideoSDKShareType type, + bool isMyShare) + { + // Don't subscribe to our own share (we're sending it) + if (isMyShare) { + return; + } + + // Get share canvas from the action + IZoomVideoSDKCanvas* shareCanvas = action->getShareCanvas(); + if (!shareCanvas) { + return; + } + + // Choose aspect based on share type + ZoomVideoSDKVideoAspect aspect = ZoomVideoSDKVideoAspect_Original; + if (type == ZoomVideoSDKShareType_Camera) { + // Camera share might benefit from pan-and-scan + aspect = ZoomVideoSDKVideoAspect_PanAndScan; + } + + // Subscribe to the share + ZoomVideoSDKErrors ret = shareCanvas->subscribeWithView( + mainShareWindow_, + aspect + ); + + if (ret == ZoomVideoSDKErrors_Success) { + ShareSubscription sub = {action, user, type, true}; + activeSubscriptions_.push_back(sub); + } + } + + void HandleShareResume(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + // Check if we need to re-subscribe + auto* sub = FindSubscription(action); + if (sub && !sub->isSubscribed) { + IZoomVideoSDKCanvas* canvas = action->getShareCanvas(); + if (canvas) { + canvas->subscribeWithView(mainShareWindow_, + ZoomVideoSDKVideoAspect_Original); + sub->isSubscribed = true; + } + } + } + + void HandleSharePause(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + // Optionally show a "Share Paused" UI overlay + // The canvas subscription remains active + auto* sub = FindSubscription(action); + if (sub) { + // You could trigger UI update here + } + } + + void HandleShareStop(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + auto* sub = FindSubscription(action); + if (!sub) return; + + // Unsubscribe from canvas + IZoomVideoSDKCanvas* canvas = action->getShareCanvas(); + if (canvas && sub->isSubscribed) { + canvas->unSubscribeWithView(mainShareWindow_); + } + + // Remove from tracking + RemoveSubscription(action); + + // Clear the share window or show placeholder + InvalidateRect(mainShareWindow_, NULL, TRUE); + } + + ShareSubscription* FindSubscription(IZoomVideoSDKShareAction* action) { + for (auto& sub : activeSubscriptions_) { + if (sub.action == action) { + return ⊂ + } + } + return nullptr; + } + + void RemoveSubscription(IZoomVideoSDKShareAction* action) { + activeSubscriptions_.erase( + std::remove_if(activeSubscriptions_.begin(), + activeSubscriptions_.end(), + [action](const ShareSubscription& s) { + return s.action == action; + }), + activeSubscriptions_.end() + ); + } + +public: + // Cleanup all subscriptions (call on session leave) + void CleanupAllShares() { + for (auto& sub : activeSubscriptions_) { + if (sub.isSubscribed && sub.action) { + IZoomVideoSDKCanvas* canvas = sub.action->getShareCanvas(); + if (canvas) { + canvas->unSubscribeWithView(mainShareWindow_); + } + } + } + activeSubscriptions_.clear(); + } + + // ========================================== + // Implement remaining IZoomVideoSDKDelegate methods as empty + // (required but not shown for brevity) + // ========================================== + void onSessionJoin() override {} + void onSessionLeave() override { CleanupAllShares(); } + void onError(ZoomVideoSDKErrors errorCode) override {} + void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, IVideoSDKVector*) override {} + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper*, IVideoSDKVector*) override {} + // ... implement all other required callbacks as empty +}; +``` + +## Share Types + +The `IZoomVideoSDKShareAction::getShareType()` returns: + +| Type | Description | +|------|-------------| +| `ZoomVideoSDKShareType_Normal` | Desktop/window share | +| `ZoomVideoSDKShareType_Camera` | Camera share (second camera) | + +## Share Status Flow + +``` +User starts sharing + │ + ▼ +onUserShareStatusChanged (status = Start) + │ + ├──► Subscribe to share canvas + │ + ▼ +[Share is active and visible] + │ + ├──► User pauses share + │ │ + │ ▼ + │ onUserShareStatusChanged (status = Pause) + │ │ + │ ├──► Show "paused" UI (optional) + │ │ + │ ▼ + │ [Share paused] + │ │ + │ ├──► User resumes share + │ │ │ + │ │ ▼ + │ │ onUserShareStatusChanged (status = Resume) + │ │ │ + │ │ └──► Re-subscribe if needed + │ │ + ▼ ▼ +[Share continues...] + │ + ▼ +User stops sharing + │ + ▼ +onUserShareStatusChanged (status = Stop) + │ + ├──► Unsubscribe from canvas + ├──► Clear share window + └──► Remove from tracking +``` + +## Best Practices + +### 1. Always Use ShareAction from Callback + +```cpp +// CORRECT +void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* pShareAction) { + pShareAction->getShareCanvas()->subscribeWithView(...); +} + +// WRONG +user->GetShareCanvas()->subscribeWithView(...); +``` + +### 2. Track Subscriptions for Cleanup + +```cpp +std::map subscribedShares_; + +// Subscribe +subscribedShares_[pShareAction] = true; + +// On session leave - cleanup all +for (auto& pair : subscribedShares_) { + // Unsubscribe each +} +``` + +### 3. Don't Subscribe to Your Own Share + +```cpp +IZoomVideoSDKUser* myself = session->getMyself(); +if (pUser == myself) { + return; // Don't subscribe to our own share +} +``` + +### 4. Handle All Share Statuses + +```cpp +switch (status) { + case ZoomVideoSDKShareStatus_Start: // New share started + case ZoomVideoSDKShareStatus_Resume: // Paused share resumed + case ZoomVideoSDKShareStatus_Pause: // Share temporarily paused + case ZoomVideoSDKShareStatus_Stop: // Share ended +} +``` + +### 5. Use Appropriate Aspect Ratio + +```cpp +// For screen share - show all content +ZoomVideoSDKVideoAspect_Original // Letterbox, no crop + +// For camera share - fill window +ZoomVideoSDKVideoAspect_PanAndScan // May crop edges +``` + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Share not visible | Using `user->GetShareCanvas()` | Use `pShareAction->getShareCanvas()` from callback | +| Multiple shares not handled | Not tracking by ShareAction | Use map keyed by `IZoomVideoSDKShareAction*` | +| Share doesn't update | Not handling Resume status | Subscribe on both Start and Resume | +| Crash on session leave | Not unsubscribing | Call `unSubscribeWithView` before cleanup | +| Can see own share | Not filtering self | Check `pUser == session->getMyself()` | + +## Related Documentation + +- **[Video Rendering](video-rendering.md)** - Video subscription (different pattern) +- **[SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md)** - Universal pattern +- **[Singleton Hierarchy](../concepts/singleton-hierarchy.md)** - SDK navigation +- **[Delegate Methods](../references/delegate-methods.md)** - All callback methods + +--- + +**Key Takeaway**: Always get the share canvas from `IZoomVideoSDKShareAction*` in the callback, never from the user object directly. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-audio.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-audio.md new file mode 100644 index 00000000..083132af --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-audio.md @@ -0,0 +1,343 @@ +# Send Raw Audio + +Complete working code for sending custom audio as a virtual microphone. + +**Official Sample**: `VSDK_sendRawAudio` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Inject custom audio into your session. Use cases: +- Virtual microphone with custom content +- Text-to-speech output +- Pre-recorded audio playback +- Audio effects/processing pipeline + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AUDIO INJECTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Your Audio Source (file, TTS, generated) │ +│ ↓ │ +│ PCM format (16-bit, 32000 Hz) │ +│ ↓ │ +│ IZoomVideoSDKVirtualAudioMic::onMicInitialize() │ +│ ↓ │ +│ IZoomVideoSDKAudioSender::send() │ +│ ↓ │ +│ Participants hear your custom audio │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Audio Format Requirements + +| Property | Value | +|----------|-------| +| Format | PCM (uncompressed) | +| Sample Rate | 32000 Hz (required) | +| Bit Depth | 16-bit signed | +| Channels | Mono (1) | +| Byte Order | Little-endian | + +**Important**: The SDK requires exactly 32000 Hz sample rate. + +--- + +## Complete Working Code + +### VirtualMic.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VirtualMic : public IZoomVideoSDKVirtualAudioMic { +public: + VirtualMic(); + ~VirtualMic(); + + // Set audio source file + void SetAudioFile(const std::string& pcmFile); + + // IZoomVideoSDKVirtualAudioMic interface + void onMicInitialize(IZoomVideoSDKAudioSender* sender) override; + void onMicStartSend() override; + void onMicStopSend() override; + void onMicUninitialized() override; + +private: + void SendLoop(); + void GenerateSineWave(char* buffer, int samples, int frequency); + + IZoomVideoSDKAudioSender* m_sender; + std::thread m_sendThread; + std::atomic m_sending; + std::string m_audioFile; + std::ifstream m_fileStream; + + static const int SAMPLE_RATE = 32000; + static const int CHANNELS = 1; + static const int BITS_PER_SAMPLE = 16; +}; +``` + +### VirtualMic.cpp + +```cpp +#include "VirtualMic.h" +#include +#include +#include + +VirtualMic::VirtualMic() + : m_sender(nullptr) + , m_sending(false) { +} + +VirtualMic::~VirtualMic() { + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VirtualMic::SetAudioFile(const std::string& pcmFile) { + m_audioFile = pcmFile; +} + +void VirtualMic::onMicInitialize(IZoomVideoSDKAudioSender* sender) { + std::cout << "VirtualMic initialized" << std::endl; + m_sender = sender; +} + +void VirtualMic::onMicStartSend() { + std::cout << "VirtualMic start sending" << std::endl; + + // Open audio file if specified + if (!m_audioFile.empty()) { + m_fileStream.open(m_audioFile, std::ios::binary); + if (!m_fileStream.is_open()) { + std::cerr << "Failed to open: " << m_audioFile << std::endl; + } + } + + m_sending = true; + m_sendThread = std::thread(&VirtualMic::SendLoop, this); +} + +void VirtualMic::onMicStopSend() { + std::cout << "VirtualMic stop sending" << std::endl; + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } + + if (m_fileStream.is_open()) { + m_fileStream.close(); + } +} + +void VirtualMic::onMicUninitialized() { + std::cout << "VirtualMic uninitialized" << std::endl; + m_sender = nullptr; +} + +void VirtualMic::SendLoop() { + // Send 20ms chunks (640 samples at 32000 Hz) + const int SAMPLES_PER_CHUNK = 640; + const int BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * sizeof(short); + + char* buffer = new char[BYTES_PER_CHUNK]; + int sampleOffset = 0; + + auto chunkInterval = std::chrono::milliseconds(20); + + while (m_sending && m_sender) { + auto startTime = std::chrono::steady_clock::now(); + + bool hasData = false; + + // Read from file or generate tone + if (m_fileStream.is_open() && m_fileStream.good()) { + m_fileStream.read(buffer, BYTES_PER_CHUNK); + hasData = m_fileStream.gcount() > 0; + + // Loop file + if (!hasData) { + m_fileStream.clear(); + m_fileStream.seekg(0); + m_fileStream.read(buffer, BYTES_PER_CHUNK); + hasData = m_fileStream.gcount() > 0; + } + } else { + // Generate 440 Hz sine wave (A4 note) + GenerateSineWave(buffer, SAMPLES_PER_CHUNK, 440); + sampleOffset += SAMPLES_PER_CHUNK; + hasData = true; + } + + if (hasData) { + // Send audio chunk + ZoomVideoSDKErrors err = m_sender->send( + buffer, + BYTES_PER_CHUNK, + SAMPLE_RATE + ); + + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "Audio send error: " << err << std::endl; + } + } + + // Maintain timing + auto elapsed = std::chrono::steady_clock::now() - startTime; + auto sleepTime = chunkInterval - elapsed; + if (sleepTime.count() > 0) { + std::this_thread::sleep_for(sleepTime); + } + } + + delete[] buffer; +} + +void VirtualMic::GenerateSineWave(char* buffer, int samples, int frequency) { + short* samples16 = reinterpret_cast(buffer); + static int phase = 0; + + const double amplitude = 16000; // ~50% volume + const double twoPi = 2.0 * 3.14159265358979323846; + + for (int i = 0; i < samples; i++) { + double t = (double)(phase + i) / SAMPLE_RATE; + double value = amplitude * sin(twoPi * frequency * t); + samples16[i] = (short)value; + } + + phase += samples; +} +``` + +### Registering the Virtual Mic + +```cpp +void StartVirtualMic() { + IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); + if (!audioHelper) return; + + // Create virtual mic + VirtualMic* virtualMic = new VirtualMic(); + + // Optional: set audio file + // virtualMic->SetAudioFile("audio.pcm"); + + // Set as audio source + ZoomVideoSDKErrors err = audioHelper->setExternalAudioSource(virtualMic); + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "setExternalAudioSource failed: " << err << std::endl; + return; + } + + // Start audio (this triggers onMicStartSend) + audioHelper->startAudio(); +} +``` + +--- + +## Audio File Preparation + +### Convert WAV to PCM + +```cmd +ffmpeg -i input.wav -f s16le -ar 32000 -ac 1 output.pcm +``` + +### Convert MP3 to PCM + +```cmd +ffmpeg -i input.mp3 -f s16le -ar 32000 -ac 1 output.pcm +``` + +### Key FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-f s16le` | 16-bit signed little-endian PCM | +| `-ar 32000` | Sample rate 32000 Hz (required!) | +| `-ac 1` | Mono channel | + +--- + +## IZoomVideoSDKAudioSender::send() + +```cpp +ZoomVideoSDKErrors send( + char* data, // PCM audio buffer + unsigned int length, // Buffer size in bytes + int sampleRate // Must be 32000 +); +``` + +**Timing**: Send 20ms chunks (640 samples = 1280 bytes at 32000 Hz mono) + +--- + +## Common Issues + +### No Audio Output + +**Cause**: Not calling `startAudio()` after setting source + +**Fix**: +```cpp +audioHelper->setExternalAudioSource(virtualMic); +audioHelper->startAudio(); // Required! +``` + +### Audio is Choppy + +**Cause**: Inconsistent send timing + +**Fix**: Use precise 20ms intervals: +```cpp +auto chunkInterval = std::chrono::milliseconds(20); +// ... send chunk ... +auto elapsed = std::chrono::steady_clock::now() - startTime; +std::this_thread::sleep_for(chunkInterval - elapsed); +``` + +### Wrong Sample Rate + +**Cause**: Using 44100 Hz instead of 32000 Hz + +**Fix**: Always use 32000 Hz: +```cmd +ffmpeg -ar 32000 ... # Not 44100! +``` + +### Audio Too Loud/Quiet + +**Cause**: PCM amplitude out of range + +**Fix**: Normalize to ±32767 range: +```cpp +const double amplitude = 16000; // 50% volume +``` + +--- + +## Related Documentation + +- [Raw Audio Capture](raw-audio-capture.md) - Capture audio +- [Send Raw Video](send-raw-video.md) - Video injection +- [Session Join Pattern](session-join-pattern.md) - Audio setup +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-video.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-video.md new file mode 100644 index 00000000..bde034e2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/send-raw-video.md @@ -0,0 +1,368 @@ +# Send Raw Video + +Complete working code for sending custom video as a virtual camera. + +**Official Sample**: `VSDK_sendRawVideo` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Inject custom video frames into your session. Use cases: +- Virtual camera with custom content +- Video filters/effects +- Pre-recorded video playback +- Computer-generated graphics + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VIDEO INJECTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Your Video Source (file, camera, generated) │ +│ ↓ │ +│ Convert to YUV420 (I420) │ +│ ↓ │ +│ IZoomVideoSDKVideoSource::onInitialize() │ +│ ↓ │ +│ IZoomVideoSDKVideoSender::sendVideoFrame() │ +│ ↓ │ +│ Participants see your custom video │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Video Format Requirements + +| Property | Value | +|----------|-------| +| Format | YUV420 (I420) planar | +| Color Space | YUV (not RGB) | +| Supported Resolutions | 90p, 180p, 360p, 720p, 1080p | +| Frame Rate | Up to 30 fps | + +--- + +## Complete Working Code + +### VideoSource.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VideoSource : public IZoomVideoSDKVideoSource { +public: + VideoSource(); + ~VideoSource(); + + // IZoomVideoSDKVideoSource interface + void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* support_cap_list, + VideoSourceCapability& suggest_cap) override; + void onPropertyChange(IVideoSDKVector* support_cap_list, + VideoSourceCapability suggest_cap) override; + void onStartSend() override; + void onStopSend() override; + void onUninitialized() override; + +private: + void SendLoop(); + void GenerateTestFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, int frameNum); + + IZoomVideoSDKVideoSender* m_sender; + std::thread m_sendThread; + std::atomic m_sending; + int m_width; + int m_height; + int m_fps; +}; +``` + +### VideoSource.cpp + +```cpp +#include "VideoSource.h" +#include +#include + +VideoSource::VideoSource() + : m_sender(nullptr) + , m_sending(false) + , m_width(1280) + , m_height(720) + , m_fps(30) { +} + +VideoSource::~VideoSource() { + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VideoSource::onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* support_cap_list, + VideoSourceCapability& suggest_cap) { + std::cout << "VideoSource initialized" << std::endl; + m_sender = sender; + + // Use suggested capability + m_width = suggest_cap.width; + m_height = suggest_cap.height; + m_fps = suggest_cap.frame; + + std::cout << "Resolution: " << m_width << "x" << m_height + << " @ " << m_fps << " fps" << std::endl; +} + +void VideoSource::onPropertyChange(IVideoSDKVector* support_cap_list, + VideoSourceCapability suggest_cap) { + std::cout << "Property changed: " << suggest_cap.width << "x" + << suggest_cap.height << std::endl; + m_width = suggest_cap.width; + m_height = suggest_cap.height; + m_fps = suggest_cap.frame; +} + +void VideoSource::onStartSend() { + std::cout << "Start sending video" << std::endl; + m_sending = true; + m_sendThread = std::thread(&VideoSource::SendLoop, this); +} + +void VideoSource::onStopSend() { + std::cout << "Stop sending video" << std::endl; + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VideoSource::onUninitialized() { + std::cout << "VideoSource uninitialized" << std::endl; + m_sender = nullptr; +} + +void VideoSource::SendLoop() { + int frameNum = 0; + auto frameInterval = std::chrono::milliseconds(1000 / m_fps); + + // Allocate YUV buffers + size_t ySize = m_width * m_height; + size_t uvSize = ySize / 4; + + char* yBuffer = new char[ySize]; + char* uBuffer = new char[uvSize]; + char* vBuffer = new char[uvSize]; + + while (m_sending && m_sender) { + auto startTime = std::chrono::steady_clock::now(); + + // Generate or load frame + GenerateTestFrame(yBuffer, uBuffer, vBuffer, m_width, m_height, frameNum); + + // Send frame + ZoomVideoSDKErrors err = m_sender->sendVideoFrame( + yBuffer, + uBuffer, + vBuffer, + m_width, + m_height, + ySize, + 0 // rotation + ); + + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "sendVideoFrame error: " << err << std::endl; + } + + frameNum++; + + // Maintain frame rate + auto elapsed = std::chrono::steady_clock::now() - startTime; + auto sleepTime = frameInterval - elapsed; + if (sleepTime.count() > 0) { + std::this_thread::sleep_for(sleepTime); + } + } + + delete[] yBuffer; + delete[] uBuffer; + delete[] vBuffer; + + std::cout << "Sent " << frameNum << " frames" << std::endl; +} + +void VideoSource::GenerateTestFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, int frameNum) { + // Generate a simple color gradient that changes over time + int colorShift = frameNum % 256; + + // Y plane (brightness) + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Gradient pattern + int brightness = ((x + colorShift) % 256); + yBuffer[y * width + x] = (char)brightness; + } + } + + // U and V planes (color) + int uvWidth = width / 2; + int uvHeight = height / 2; + + for (int y = 0; y < uvHeight; y++) { + for (int x = 0; x < uvWidth; x++) { + int index = y * uvWidth + x; + uBuffer[index] = (char)(128 + (colorShift / 2)); // Blue tint + vBuffer[index] = (char)(128 - (colorShift / 2)); // Red tint + } + } +} +``` + +### Registering the Video Source + +```cpp +void StartVirtualCamera() { + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + if (!videoHelper) return; + + // Create and set video source + VideoSource* videoSource = new VideoSource(); + + ZoomVideoSDKErrors err = videoHelper->setExternalVideoSource(videoSource); + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "setExternalVideoSource failed: " << err << std::endl; + return; + } + + // Start video (this triggers onStartSend) + videoHelper->startVideo(); +} + +void StopVirtualCamera() { + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + if (videoHelper) { + videoHelper->stopVideo(); + } +} +``` + +--- + +## Loading Video from File + +### Read YUV File + +```cpp +bool LoadYUVFrame(const std::string& filename, int frameIndex, + char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) return false; + + size_t ySize = width * height; + size_t uvSize = ySize / 4; + size_t frameSize = ySize + uvSize * 2; + + // Seek to frame + file.seekg(frameIndex * frameSize); + + // Read YUV planes + file.read(yBuffer, ySize); + file.read(uBuffer, uvSize); + file.read(vBuffer, uvSize); + + return file.good(); +} +``` + +### Convert RGB to YUV + +```cpp +void RGBToYUV420(unsigned char* rgb, char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgbIndex = (y * width + x) * 3; + int R = rgb[rgbIndex + 2]; // BGR order + int G = rgb[rgbIndex + 1]; + int B = rgb[rgbIndex + 0]; + + // RGB to YUV conversion (BT.601) + int Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; + + yBuffer[y * width + x] = (char)Y; + + // U and V at quarter resolution + if (y % 2 == 0 && x % 2 == 0) { + int uvIndex = (y / 2) * (width / 2) + (x / 2); + int U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; + int V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; + + uBuffer[uvIndex] = (char)U; + vBuffer[uvIndex] = (char)V; + } + } + } +} +``` + +--- + +## sendVideoFrame Parameters + +```cpp +ZoomVideoSDKErrors sendVideoFrame( + char* frameBuffer, // Y plane or full frame + char* uBuffer, // U plane (can be nullptr for NV12) + char* vBuffer, // V plane (can be nullptr for NV12) + int width, // Frame width + int height, // Frame height + int frameLength, // Y plane size (width * height) + int rotation // 0, 90, 180, 270 +); +``` + +--- + +## Common Issues + +### Video Not Showing + +**Cause**: Not calling `startVideo()` after setting source + +**Fix**: +```cpp +videoHelper->setExternalVideoSource(source); +videoHelper->startVideo(); // Required! +``` + +### Frame Rate Too Low + +**Cause**: Frame generation slower than frame rate + +**Fix**: Optimize frame generation or reduce resolution + +### Color Looks Wrong + +**Cause**: RGB/BGR order mismatch or wrong YUV conversion + +**Fix**: Verify color space conversion formula + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Capture video +- [Send Raw Audio](send-raw-audio.md) - Audio injection +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Rendering approaches +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/session-join-pattern.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/session-join-pattern.md new file mode 100644 index 00000000..e9aae1ff --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/session-join-pattern.md @@ -0,0 +1,416 @@ +# Session Join Pattern + +Complete working code for initializing the SDK and joining a session. + +## Overview + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AddListener│───►│ JoinSession │───►│ onSessionJoin│ +│ │ │ (delegate) │ │ (JWT) │ │ callback │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +``` + +--- + +## Complete Working Code + +### main.cpp + +```cpp +#include +#include +#include +#include +#include +#include +#include + +// SDK headers +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +// JSON parsing (optional - for config file) +#include + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +// Global state +IZoomVideoSDK* g_sdk = nullptr; +bool g_inSession = false; +bool g_exit = false; + +// Configuration +std::wstring g_jwt; +std::wstring g_sessionName; +std::wstring g_sessionPassword; +std::wstring g_userName; + +//───────────────────────────────────────────────────────────────────────────── +// DELEGATE IMPLEMENTATION +//───────────────────────────────────────────────────────────────────────────── + +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // === SESSION LIFECYCLE === + + void onSessionJoin() override { + std::cout << "[EVENT] Session joined successfully!" << std::endl; + g_inSession = true; + + // Connect audio + IZoomVideoSDKAudioHelper* audioHelper = g_sdk->getAudioHelper(); + if (audioHelper) { + audioHelper->startAudio(); + std::cout << "[ACTION] Audio connected" << std::endl; + } + + // Start video + IZoomVideoSDKVideoHelper* videoHelper = g_sdk->getVideoHelper(); + if (videoHelper) { + videoHelper->startVideo(); + std::cout << "[ACTION] Video started" << std::endl; + } + } + + void onSessionLeave() override { + std::cout << "[EVENT] Session left" << std::endl; + g_inSession = false; + g_exit = true; + } + + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { + std::cout << "[ERROR] Code: " << errorCode + << ", Detail: " << detailErrorCode << std::endl; + } + + // === USER EVENTS === + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + std::wcout << L"[EVENT] User joined: " << user->getUserName() << std::endl; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + std::wcout << L"[EVENT] User left: " << user->getUserName() << std::endl; + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + bool isOn = user->GetVideoPipe()->getVideoStatus().isOn; + std::wcout << L"[EVENT] Video " << (isOn ? L"ON" : L"OFF") + << L": " << user->getUserName() << std::endl; + } + } + + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + bool isMuted = user->getAudioStatus().isMuted; + std::wcout << L"[EVENT] Audio " << (isMuted ? L"muted" : L"unmuted") + << L": " << user->getUserName() << std::endl; + } + } + + // === CHAT === + + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* msg) override { + std::wcout << L"[CHAT] " << msg->getSendUser()->getUserName() + << L": " << msg->getContent() << std::endl; + } + + // === REQUIRED EMPTY IMPLEMENTATIONS === + // (All pure virtual methods must be implemented) + + void onSessionLeave(ZoomVideoSDKSessionLeaveReason reason) override {} + void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) override {} + void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) override {} + void onUserHostChanged(IZoomVideoSDKUserHelper* helper, IZoomVideoSDKUser* user) override {} + void onUserManagerChanged(IZoomVideoSDKUser* user) override {} + void onUserNameChanged(IZoomVideoSDKUser* user) override {} + void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* helper, IVideoSDKVector* list) override {} + void onMixedAudioRawDataReceived(AudioRawData* data) override {} + void onOneWayAudioRawDataReceived(AudioRawData* data, IZoomVideoSDKUser* user) override {} + void onSharedAudioRawDataReceived(AudioRawData* data) override {} + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onShareContentChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onFailedToStartShare(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user) override {} + void onShareContentSizeChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* helper, ZoomVideoSDKLiveStreamStatus status) override {} + void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* helper, const zchar_t* msgID, ZoomVideoSDKChatMessageDeleteType type) override {} + void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* helper, ZoomVideoSDKChatPrivilegeType privilege) override {} + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override {} + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override {} + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override {} + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spoken, ILiveTranscriptionLanguage* transcript) override {} + void onProxyDetectComplete() override {} + void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) override {} + void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) override {} + void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, IZoomVideoSDKUser* user) override {} + void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) override {} + void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, IZoomVideoSDKUser* user, void* handle) override {} + void onShareCanvasSubscribeFail(IZoomVideoSDKUser* user, void* handle, IZoomVideoSDKShareAction* action) override {} + void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) override {} + void onAnnotationPrivilegeChange(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onAnnotationHelperActived(void* handle) override {} + void onSendFileStatus(IZoomVideoSDKSendFile* file, const FileTransferStatus& status) override {} + void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, const FileTransferStatus& status) override {} + void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) override {} + void onCalloutJoinSuccess(IZoomVideoSDKUser* user, const zchar_t* phoneNumber) override {} + void onCameraControlRequestResult(IZoomVideoSDKUser* user, bool approved) override {} + void onCameraControlRequestReceived(IZoomVideoSDKUser* user, ZoomVideoSDKCameraControlRequestType type, IZoomVideoSDKCameraControlRequestHandler* handler) override {} + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* cmd) override {} + void onCommandChannelConnectResult(bool success) override {} + void onCloudRecordingStatus(RecordingStatus status, IZoomVideoSDKRecordingConsentHandler* handler) override {} + void onHostAskUnmute() override {} + void onUserRecordingConsent(IZoomVideoSDKUser* user) override {} + void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, IZoomVideoSDKUser* user, IZoomVideoSDKRawDataPipe* pipe) override {} + void onMicSpeakerVolumeChanged(unsigned int micVol, unsigned int speakerVol) override {} + void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, ZoomVideoSDKAudioDeviceStatus status) override {} + void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) override {} + void onSelectedAudioDeviceChanged() override {} + void onCameraListChanged() override {} + void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* helper, IVideoSDKVector* list) override {} + void onVideoAlphaChannelStatusChanged(bool isOn) override {} + void onRemoteControlStatus(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action, ZoomVideoSDKRemoteControlStatus status) override {} + void onRemoteControlRequestReceived(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action, IZoomVideoSDKRemoteControlRequestHandler* handler) override {} + void onRemoteControlServiceInstallResult(bool success) override {} + void onBindIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onUnbindIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onIncomingLiveStreamStatusResponse(bool success, IVideoSDKVector* list) override {} + void onStartIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onStopIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser* user, IZoomVideoSDKWhiteboardHelper* helper) override {} +}; + +//───────────────────────────────────────────────────────────────────────────── +// CONFIGURATION +//───────────────────────────────────────────────────────────────────────────── + +bool LoadConfig(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + std::cerr << "Cannot open " << filename << std::endl; + return false; + } + + Json::Value config; + file >> config; + + std::string jwt = config["jwt"].asString(); + std::string sessionName = config["session_name"].asString(); + std::string password = config.get("password", "").asString(); + std::string userName = config.get("user_name", "Bot").asString(); + + g_jwt = std::wstring(jwt.begin(), jwt.end()); + g_sessionName = std::wstring(sessionName.begin(), sessionName.end()); + g_sessionPassword = std::wstring(password.begin(), password.end()); + g_userName = std::wstring(userName.begin(), userName.end()); + + return true; +} + +//───────────────────────────────────────────────────────────────────────────── +// SDK OPERATIONS +//───────────────────────────────────────────────────────────────────────────── + +bool InitializeSDK() { + g_sdk = CreateZoomVideoSDKObj(); + if (!g_sdk) { + std::cerr << "Failed to create SDK object" << std::endl; + return false; + } + + ZoomVideoSDKInitParams params; + params.domain = L"https://zoom.us"; + params.enableLog = true; + params.logFilePrefix = L"zoom_video_sdk"; + params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + + ZoomVideoSDKErrors err = g_sdk->initialize(params); + if (err != ZoomVideoSDKErrors_Success) { + std::cerr << "SDK initialize failed: " << err << std::endl; + return false; + } + + std::cout << "SDK initialized successfully" << std::endl; + return true; +} + +bool JoinSession() { + // Register delegate BEFORE joining + g_sdk->addListener(new MyDelegate()); + + ZoomVideoSDKSessionContext context; + context.sessionName = g_sessionName.c_str(); + context.userName = g_userName.c_str(); + context.token = g_jwt.c_str(); + context.sessionPassword = g_sessionPassword.c_str(); + + // IMPORTANT: Connect audio in onSessionJoin callback + context.audioOption.connect = false; + context.audioOption.mute = true; + context.videoOption.localVideoOn = false; + + IZoomVideoSDKSession* session = g_sdk->joinSession(context); + if (!session) { + std::cerr << "joinSession returned null" << std::endl; + return false; + } + + std::cout << "Join session initiated..." << std::endl; + return true; +} + +void Cleanup() { + if (g_sdk) { + if (g_inSession) { + g_sdk->leaveSession(false); + } + g_sdk->cleanup(); + DestroyZoomVideoSDKObj(); + g_sdk = nullptr; + } +} + +//───────────────────────────────────────────────────────────────────────────── +// MAIN +//───────────────────────────────────────────────────────────────────────────── + +int main() { + // Initialize COM (required for some SDK features) + CoInitialize(NULL); + + // Load configuration + if (!LoadConfig("config.json")) { + return 1; + } + + // Initialize SDK + if (!InitializeSDK()) { + return 1; + } + + // Join session + if (!JoinSession()) { + Cleanup(); + return 1; + } + + // ═══════════════════════════════════════════════════════════════════════ + // CRITICAL: Windows message loop + // Without this, callbacks will NEVER fire! + // ═══════════════════════════════════════════════════════════════════════ + std::cout << "Running message loop (Ctrl+C to exit)..." << std::endl; + + MSG msg; + while (!g_exit) { + // Process all pending Windows messages + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Cleanup + Cleanup(); + CoUninitialize(); + + std::cout << "Exited cleanly" << std::endl; + return 0; +} +``` + +### config.json + +```json +{ + "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "session_name": "my-session", + "password": "", + "user_name": "Bot" +} +``` + +--- + +## Key Points + +### 1. Register Delegate BEFORE Joining + +```cpp +// CORRECT +g_sdk->addListener(new MyDelegate()); +g_sdk->joinSession(context); + +// WRONG - callbacks will be missed +g_sdk->joinSession(context); +g_sdk->addListener(new MyDelegate()); // Too late! +``` + +### 2. Set audioOption.connect = false + +```cpp +context.audioOption.connect = false; // Connect in onSessionJoin +context.audioOption.mute = true; +``` + +Then connect audio in the callback: + +```cpp +void onSessionJoin() override { + g_sdk->getAudioHelper()->startAudio(); +} +``` + +### 3. Windows Message Loop is MANDATORY + +```cpp +// This is NOT optional! +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### 4. Implement ALL Delegate Methods + +The `IZoomVideoSDKDelegate` interface has 80+ pure virtual methods. **All must be implemented**, even if empty. + +--- + +## Related Documentation + +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Why message loop is critical +- [Delegate Methods](../references/delegate-methods.md) - All 80+ callback methods +- [Video Rendering](video-rendering.md) - Subscribe to video after join +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal pattern + +--- + +**TL;DR**: Initialize → Add delegate → Join with `audioOption.connect = false` → Run message loop → Connect audio in `onSessionJoin`. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/transcription.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/transcription.md new file mode 100644 index 00000000..e24421b4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/transcription.md @@ -0,0 +1,405 @@ +# Live Transcription + +Complete working code for real-time speech-to-text transcription. + +**Official Sample**: `VSDK_TranscriptionAndTranslation` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Live transcription provides real-time captions of speech. Features: +- Automatic speech recognition +- Multiple language support +- Speaker identification +- Translation (optional) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TRANSCRIPTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Enable transcription → startLiveTranscription() │ +│ 2. Receive captions → onLiveTranscriptionMsgInfoReceived() │ +│ 3. Display/process text │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +- Live transcription must be enabled for the session +- Valid Zoom account with transcription feature + +--- + +## Complete Working Code + +### TranscriptionManager.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +struct TranscriptionMessage { + std::wstring speakerName; + std::wstring text; + bool isFinal; + unsigned long long timestamp; +}; + +class TranscriptionManager { +public: + TranscriptionManager(IZoomVideoSDK* sdk); + + // Control + bool StartTranscription(); + bool StopTranscription(); + bool IsTranscribing() const { return m_isTranscribing; } + + // Language settings + bool SetSpokenLanguage(ILiveTranscriptionLanguage* language); + bool SetTranslationLanguage(ILiveTranscriptionLanguage* language); + std::vector GetAvailableLanguages(); + + // Callbacks from delegate + void OnStatusChanged(ZoomVideoSDKLiveTranscriptionStatus status); + void OnMessageReceived(ILiveTranscriptionMessageInfo* info); + void OnOriginalLanguageReceived(ILiveTranscriptionMessageInfo* info); + void OnError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript); + + // Set message handler + using MessageCallback = std::function; + void SetMessageHandler(MessageCallback callback) { m_callback = callback; } + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKLiveTranscriptionHelper* m_helper; + bool m_isTranscribing; + MessageCallback m_callback; +}; +``` + +### TranscriptionManager.cpp + +```cpp +#include "TranscriptionManager.h" +#include + +TranscriptionManager::TranscriptionManager(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_helper(nullptr) + , m_isTranscribing(false) { +} + +bool TranscriptionManager::StartTranscription() { + m_helper = m_sdk->getLiveTranscriptionHelper(); + if (!m_helper) { + std::cout << "Transcription helper not available" << std::endl; + return false; + } + + // Check if transcription is available + if (!m_helper->canStartLiveTranscription()) { + std::cout << "Cannot start transcription" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_helper->startLiveTranscription(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Transcription started" << std::endl; + return true; + } + + std::cout << "Start transcription failed: " << err << std::endl; + return false; +} + +bool TranscriptionManager::StopTranscription() { + if (!m_helper) return false; + + ZoomVideoSDKErrors err = m_helper->stopLiveTranscription(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Transcription stopped" << std::endl; + m_isTranscribing = false; + return true; + } + + return false; +} + +std::vector TranscriptionManager::GetAvailableLanguages() { + std::vector languages; + + if (!m_helper) { + m_helper = m_sdk->getLiveTranscriptionHelper(); + } + + if (m_helper) { + IVideoSDKVector* langList = + m_helper->getAvailableSpokenLanguages(); + + if (langList) { + for (int i = 0; i < langList->GetCount(); i++) { + languages.push_back(langList->GetItem(i)); + } + } + } + + return languages; +} + +bool TranscriptionManager::SetSpokenLanguage(ILiveTranscriptionLanguage* language) { + if (!m_helper || !language) return false; + + ZoomVideoSDKErrors err = m_helper->setSpokenLanguage(language); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Spoken language set to: " + << language->getLTTLanguageName() << std::endl; + return true; + } + return false; +} + +bool TranscriptionManager::SetTranslationLanguage(ILiveTranscriptionLanguage* language) { + if (!m_helper || !language) return false; + + ZoomVideoSDKErrors err = m_helper->setTranslationLanguage(language); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Translation language set to: " + << language->getLTTLanguageName() << std::endl; + return true; + } + return false; +} + +void TranscriptionManager::OnStatusChanged(ZoomVideoSDKLiveTranscriptionStatus status) { + switch (status) { + case ZoomVideoSDKLiveTranscription_Status_Start: + std::cout << "Transcription started" << std::endl; + m_isTranscribing = true; + break; + + case ZoomVideoSDKLiveTranscription_Status_Stop: + std::cout << "Transcription stopped" << std::endl; + m_isTranscribing = false; + break; + + case ZoomVideoSDKLiveTranscription_Status_Connecting: + std::cout << "Transcription connecting..." << std::endl; + break; + + default: + std::cout << "Transcription status: " << status << std::endl; + } +} + +void TranscriptionManager::OnMessageReceived(ILiveTranscriptionMessageInfo* info) { + if (!info) return; + + TranscriptionMessage msg; + + // Get speaker + IZoomVideoSDKUser* speaker = info->getSpeaker(); + if (speaker) { + msg.speakerName = speaker->getUserName(); + } + + // Get text + const zchar_t* text = info->getMessageContent(); + if (text) { + msg.text = text; + } + + // Get metadata + msg.isFinal = (info->getMessageType() == + ZoomVideoSDKLiveTranscriptionOperationType_Complete); + msg.timestamp = info->getTimeStamp(); + + // Display + std::wcout << L"[" << msg.speakerName << L"] " << msg.text; + if (msg.isFinal) { + std::wcout << L" (final)"; + } + std::wcout << std::endl; + + // Call user handler + if (m_callback) { + m_callback(msg); + } +} + +void TranscriptionManager::OnOriginalLanguageReceived(ILiveTranscriptionMessageInfo* info) { + // Original language message (before translation) + if (!info) return; + + const zchar_t* text = info->getMessageContent(); + if (text) { + std::wcout << L"[Original] " << text << std::endl; + } +} + +void TranscriptionManager::OnError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript) { + std::cout << "Transcription error" << std::endl; + if (spoken) { + std::wcout << L"Spoken language: " << spoken->getLTTLanguageName() << std::endl; + } + if (transcript) { + std::wcout << L"Transcript language: " << transcript->getLTTLanguageName() << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + TranscriptionManager* m_transcription; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_transcription = new TranscriptionManager(sdk); + + // Set message handler + m_transcription->SetMessageHandler([](const TranscriptionMessage& msg) { + // Process transcription (e.g., save to file, analyze) + if (msg.isFinal) { + SaveTranscript(msg.speakerName, msg.text); + } + }); + } + + void onSessionJoin() override { + // Start transcription + m_transcription->StartTranscription(); + } + + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override { + m_transcription->OnStatusChanged(status); + } + + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override { + m_transcription->OnMessageReceived(info); + } + + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override { + m_transcription->OnOriginalLanguageReceived(info); + } + + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript) override { + m_transcription->OnError(spoken, transcript); + } + + // ... other callbacks +}; +``` + +--- + +## Message Types + +| Type | Description | +|------|-------------| +| `ZoomVideoSDKLiveTranscriptionOperationType_N` | Interim result (may change) | +| `ZoomVideoSDKLiveTranscriptionOperationType_Complete` | Final result | +| `ZoomVideoSDKLiveTranscriptionOperationType_Update` | Updated previous result | + +**Note**: Interim results allow real-time display but may be revised. + +--- + +## ILiveTranscriptionMessageInfo Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getMessageContent()` | `const zchar_t*` | Transcribed text | +| `getSpeaker()` | `IZoomVideoSDKUser*` | Speaker user object | +| `getTimeStamp()` | `unsigned long long` | Message timestamp | +| `getMessageType()` | `ZoomVideoSDKLiveTranscriptionOperationType` | Interim/final | +| `getMessageID()` | `const zchar_t*` | Unique message ID | + +--- + +## Language Support + +### List Available Languages + +```cpp +auto languages = transcriptionManager->GetAvailableLanguages(); +for (auto lang : languages) { + std::wcout << lang->getLTTLanguageID() << L": " + << lang->getLTTLanguageName() << std::endl; +} +``` + +### Common Language Codes + +| Code | Language | +|------|----------| +| `en` | English | +| `es` | Spanish | +| `fr` | French | +| `de` | German | +| `zh` | Chinese | +| `ja` | Japanese | + +--- + +## Common Issues + +### Transcription Not Available + +**Cause**: Feature not enabled for session + +**Fix**: Check `canStartLiveTranscription()` first: +```cpp +if (helper->canStartLiveTranscription()) { + helper->startLiveTranscription(); +} +``` + +### No Messages Received + +**Cause**: No one is speaking or audio not connected + +**Fix**: Ensure audio is connected: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); // Connect audio first + transcription->StartTranscription(); +} +``` + +### Wrong Language + +**Cause**: Spoken language not set correctly + +**Fix**: Set spoken language: +```cpp +auto languages = manager->GetAvailableLanguages(); +for (auto lang : languages) { + if (wcscmp(lang->getLTTLanguageID(), L"en") == 0) { + manager->SetSpokenLanguage(lang); + break; + } +} +``` + +--- + +## Related Documentation + +- [Raw Audio Capture](raw-audio-capture.md) - Alternative audio processing +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Transcription callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/examples/video-rendering.md b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/video-rendering.md new file mode 100644 index 00000000..c849a5da --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/examples/video-rendering.md @@ -0,0 +1,447 @@ +# Video Rendering with Canvas API + +Complete working code for subscribing to and displaying video using the Canvas API. + +## Overview + +The Canvas API lets the SDK render video directly to your window. This is the **recommended approach** for standard video applications. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VIDEO SUBSCRIPTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ onSessionJoin → Subscribe to self video │ +│ onUserVideoStatusChanged → Subscribe to remote video (when ON)│ +│ onUserLeave → Unsubscribe and cleanup │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Critical Rule + +### Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +```cpp +// WRONG - Video may not be ready yet! +void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + // This returns Error 2 (Internal_Error) - video not ready! + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } +} + +// CORRECT - Wait for video status change +void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + // Skip self (handled separately) + if (user == myself) continue; + + // Check if video is actually on + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + if (status.isOn) { + // NOW it's safe to subscribe + SubscribeToUser(user); + } else { + // Video turned off - unsubscribe + UnsubscribeFromUser(user); + } + } +} +``` + +--- + +## Complete Working Code + +### VideoCanvasManager.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VideoCanvasManager { +public: + VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk); + ~VideoCanvasManager(); + + // Subscribe to self video + void SubscribeToSelf(); + + // Subscribe/unsubscribe to remote user + void SubscribeToUser(IZoomVideoSDKUser* user); + void UnsubscribeFromUser(IZoomVideoSDKUser* user); + + // Cleanup + void UnsubscribeAll(); + + // Layout + void UpdateLayout(); + +private: + HWND CreateVideoWindow(); + void DestroyVideoWindow(HWND hwnd); + + IZoomVideoSDK* m_sdk; + HWND m_parentWindow; + + // Self video + HWND m_selfVideoWindow; + IZoomVideoSDKCanvas* m_selfCanvas; + + // Remote users: user -> window mapping + std::map m_userWindows; + std::map m_userCanvases; +}; +``` + +### VideoCanvasManager.cpp + +```cpp +#include "VideoCanvasManager.h" +#include + +VideoCanvasManager::VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk) + : m_parentWindow(parentWindow) + , m_sdk(sdk) + , m_selfVideoWindow(nullptr) + , m_selfCanvas(nullptr) { +} + +VideoCanvasManager::~VideoCanvasManager() { + UnsubscribeAll(); +} + +HWND VideoCanvasManager::CreateVideoWindow() { + // Create a child window for video rendering + HWND hwnd = CreateWindowExW( + 0, + L"STATIC", // Simple static window class + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER, + 0, 0, 320, 240, // Size will be set by UpdateLayout() + m_parentWindow, + nullptr, + GetModuleHandle(nullptr), + nullptr + ); + + return hwnd; +} + +void VideoCanvasManager::DestroyVideoWindow(HWND hwnd) { + if (hwnd) { + DestroyWindow(hwnd); + } +} + +void VideoCanvasManager::SubscribeToSelf() { + IZoomVideoSDKSession* session = m_sdk->getSessionInfo(); + if (!session) return; + + IZoomVideoSDKUser* myself = session->getMyself(); + if (!myself) return; + + // Create window for self video + if (!m_selfVideoWindow) { + m_selfVideoWindow = CreateVideoWindow(); + } + + // Subscribe + m_selfCanvas = myself->GetVideoCanvas(); + if (m_selfCanvas) { + ZoomVideoSDKErrors err = m_selfCanvas->subscribeWithView( + m_selfVideoWindow, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Self video subscribed" << std::endl; + } else { + std::cout << "Self video subscribe failed: " << err << std::endl; + } + } + + UpdateLayout(); +} + +void VideoCanvasManager::SubscribeToUser(IZoomVideoSDKUser* user) { + if (!user) return; + + // Skip if already subscribed + if (m_userWindows.find(user) != m_userWindows.end()) { + return; + } + + // Check if video is on + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (!pipe || !pipe->getVideoStatus().isOn) { + return; + } + + // Create window for this user + HWND hwnd = CreateVideoWindow(); + m_userWindows[user] = hwnd; + + // Subscribe to canvas + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + if (canvas) { + ZoomVideoSDKErrors err = canvas->subscribeWithView( + hwnd, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (err == ZoomVideoSDKErrors_Success) { + m_userCanvases[user] = canvas; + std::wcout << L"Subscribed to: " << user->getUserName() << std::endl; + } else { + std::wcout << L"Subscribe failed for: " << user->getUserName() + << L" error: " << err << std::endl; + // Cleanup on failure + DestroyVideoWindow(hwnd); + m_userWindows.erase(user); + } + } + + UpdateLayout(); +} + +void VideoCanvasManager::UnsubscribeFromUser(IZoomVideoSDKUser* user) { + if (!user) return; + + auto it = m_userWindows.find(user); + if (it == m_userWindows.end()) { + return; + } + + HWND hwnd = it->second; + + // Unsubscribe from canvas + auto canvasIt = m_userCanvases.find(user); + if (canvasIt != m_userCanvases.end()) { + IZoomVideoSDKCanvas* canvas = canvasIt->second; + if (canvas) { + canvas->unSubscribeWithView(hwnd); + } + m_userCanvases.erase(canvasIt); + } + + // Destroy window + DestroyVideoWindow(hwnd); + m_userWindows.erase(it); + + std::wcout << L"Unsubscribed from: " << user->getUserName() << std::endl; + + UpdateLayout(); +} + +void VideoCanvasManager::UnsubscribeAll() { + // Unsubscribe self + if (m_selfCanvas && m_selfVideoWindow) { + m_selfCanvas->unSubscribeWithView(m_selfVideoWindow); + DestroyVideoWindow(m_selfVideoWindow); + m_selfVideoWindow = nullptr; + m_selfCanvas = nullptr; + } + + // Unsubscribe all remote users + for (auto& pair : m_userCanvases) { + IZoomVideoSDKCanvas* canvas = pair.second; + IZoomVideoSDKUser* user = pair.first; + + auto windowIt = m_userWindows.find(user); + if (windowIt != m_userWindows.end() && canvas) { + canvas->unSubscribeWithView(windowIt->second); + } + } + + for (auto& pair : m_userWindows) { + DestroyVideoWindow(pair.second); + } + + m_userCanvases.clear(); + m_userWindows.clear(); +} + +void VideoCanvasManager::UpdateLayout() { + RECT parentRect; + GetClientRect(m_parentWindow, &parentRect); + + int totalVideos = (m_selfVideoWindow ? 1 : 0) + m_userWindows.size(); + if (totalVideos == 0) return; + + // Calculate grid dimensions + int cols = (int)ceil(sqrt((double)totalVideos)); + int rows = (int)ceil((double)totalVideos / cols); + + int cellWidth = (parentRect.right - parentRect.left) / cols; + int cellHeight = (parentRect.bottom - parentRect.top) / rows; + + int index = 0; + + // Position self video (top-left) + if (m_selfVideoWindow) { + int row = index / cols; + int col = index % cols; + SetWindowPos(m_selfVideoWindow, NULL, + col * cellWidth, row * cellHeight, + cellWidth, cellHeight, + SWP_NOZORDER); + index++; + } + + // Position remote videos + for (auto& pair : m_userWindows) { + HWND hwnd = pair.second; + int row = index / cols; + int col = index % cols; + SetWindowPos(hwnd, NULL, + col * cellWidth, row * cellHeight, + cellWidth, cellHeight, + SWP_NOZORDER); + index++; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + VideoCanvasManager* m_videoManager; + +public: + MyDelegate(HWND parentWindow, IZoomVideoSDK* sdk) { + m_videoManager = new VideoCanvasManager(parentWindow, sdk); + } + + ~MyDelegate() { + delete m_videoManager; + } + + void onSessionJoin() override { + // Subscribe to self video + m_videoManager->SubscribeToSelf(); + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user == myself) continue; + + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + if (status.isOn) { + m_videoManager->SubscribeToUser(user); + } else { + m_videoManager->UnsubscribeFromUser(user); + } + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + m_videoManager->UnsubscribeFromUser(userList->GetItem(i)); + } + } + + void onSessionLeave() override { + m_videoManager->UnsubscribeAll(); + } + + // ... other callbacks +}; +``` + +--- + +## Error Handling + +### Subscribe Fail Callback + +```cpp +void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, + IZoomVideoSDKUser* user, void* handle) override { + std::wcout << L"Subscribe failed for: " << user->getUserName() + << L" reason: " << reason << std::endl; + + switch (reason) { + case ZoomVideoSDKSubscribeFailReason_HasSubscribe1080POr720P: + std::cout << "Already have a 1080p/720p subscription" << std::endl; + break; + case ZoomVideoSDKSubscribeFailReason_HasSubscribeExceededLimit: + std::cout << "Subscription limit exceeded" << std::endl; + break; + case ZoomVideoSDKSubscribeFailReason_TooFrequentCall: + std::cout << "Calling too frequently - add Sleep(200)" << std::endl; + break; + } +} +``` + +### Subscribe Fail Reasons + +| Code | Reason | Solution | +|------|--------|----------| +| 0 | None | - | +| 1 | HasSubscribe1080POr720P | Already have HD subscription | +| 2 | HasSubscribeTwo720P | Max 2x 720p subscriptions | +| 3 | HasSubscribeExceededLimit | Too many subscriptions | +| 4 | HasSubscribeTwoShare | Max 2 share subscriptions | +| 5 | HasSubscribeVideo1080POr720PAndOneShare | Limit reached | +| 6 | TooFrequentCall | Add `Sleep(200)` between calls | + +--- + +## Aspect Ratio Options + +| Option | Behavior | Use Case | +|--------|----------|----------| +| `ZoomVideoSDKVideoAspect_Original` | Letterbox/pillarbox | Show full video | +| `ZoomVideoSDKVideoAspect_FullFilled` | Fill, may crop | Full coverage | +| `ZoomVideoSDKVideoAspect_PanAndScan` | Smart crop | Balanced (recommended) | +| `ZoomVideoSDKVideoAspect_LetterBox` | Black bars | Preserve aspect | + +--- + +## Resolution Options + +| Option | Resolution | Use Case | +|--------|------------|----------| +| `ZoomVideoSDKResolution_90P` | 160x90 | Thumbnails | +| `ZoomVideoSDKResolution_180P` | 320x180 | Small previews | +| `ZoomVideoSDKResolution_360P` | 640x360 | Standard | +| `ZoomVideoSDKResolution_720P` | 1280x720 | HD | +| `ZoomVideoSDKResolution_1080P` | 1920x1080 | Full HD | +| `ZoomVideoSDKResolution_Auto` | SDK chooses | Recommended | + +--- + +## Related Documentation + +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Rendering approach comparison +- [Raw Video Capture](raw-video-capture.md) - For custom processing +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Canvas/Pipe navigation +- [Common Issues](../troubleshooting/common-issues.md) - Error codes + +--- + +**TL;DR**: Subscribe to self in `onSessionJoin`, subscribe to remote users in `onUserVideoStatusChanged` (when `status.isOn == true`), unsubscribe in `onUserLeave`. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/references/delegate-methods.md b/partner-built/zoom-plugin/skills/video-sdk/windows/references/delegate-methods.md new file mode 100644 index 00000000..3f2df854 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/references/delegate-methods.md @@ -0,0 +1,591 @@ +# Delegate Methods Reference + +Complete list of all `IZoomVideoSDKDelegate` callback methods. **All 80+ methods must be implemented**, even if empty. + +> **Note**: The callback count has grown significantly in SDK v2.4.x with additions for subsessions (breakout rooms), broadcast streaming, whiteboard, RTMS, and enhanced annotation support. + +--- + +## Quick Template + +Copy this template and add your implementation: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // ═══════════════════════════════════════════════════════════════════════ + // SESSION LIFECYCLE + // ═══════════════════════════════════════════════════════════════════════ + + void onSessionJoin() override { + // Called when successfully joined session + } + + void onSessionLeave() override { + // Called when left session (no reason) + } + + void onSessionLeave(ZoomVideoSDKSessionLeaveReason reason) override { + // Called when left session (with reason) + } + + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { + // Called on SDK errors + } + + void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) override { + // Called when session requires password + } + + void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) override { + // Called when password is incorrect + } + + // ═══════════════════════════════════════════════════════════════════════ + // USER EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // Called when users join session + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // Called when users leave session + } + + void onUserHostChanged(IZoomVideoSDKUserHelper* helper, + IZoomVideoSDKUser* user) override { + // Called when host changes + } + + void onUserManagerChanged(IZoomVideoSDKUser* user) override { + // Called when manager status changes + } + + void onUserNameChanged(IZoomVideoSDKUser* user) override { + // Called when user name changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // VIDEO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // IMPORTANT: Subscribe to video here, not in onUserJoin + } + + void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // Called when spotlight changes + } + + void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, + IZoomVideoSDKUser* user, void* handle) override { + // Called when video subscription fails + } + + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override { + // Called when alpha channel mode changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // AUDIO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + // Called when audio status changes (mute/unmute) + } + + void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + // Called when active speaker changes + } + + void onHostAskUnmute() override { + // Called when host requests you to unmute + } + + // ═══════════════════════════════════════════════════════════════════════ + // RAW AUDIO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Called with mixed audio from all participants + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + // Called with audio from specific user + } + + void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Called with shared audio + } + + // ═══════════════════════════════════════════════════════════════════════ + // SHARE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share status changes - use shareAction to subscribe + } + + void onShareContentChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share content type changes + } + + void onFailedToStartShare(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user) override { + // Called when share fails to start + } + + void onShareContentSizeChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share size changes + } + + void onShareCanvasSubscribeFail(IZoomVideoSDKUser* user, void* handle, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share subscription fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CHAT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* messageItem) override { + // Called when new chat message received + } + + void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* helper, + const zchar_t* msgID, + ZoomVideoSDKChatMessageDeleteType deleteBy) override { + // Called when chat message deleted + } + + void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* helper, + ZoomVideoSDKChatPrivilegeType privilege) override { + // Called when chat privilege changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // COMMAND CHANNEL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) override { + // Called when command received + } + + void onCommandChannelConnectResult(bool isSuccess) override { + // Called when command channel connection result + } + + // ═══════════════════════════════════════════════════════════════════════ + // RECORDING EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + // Called when cloud recording status changes + } + + void onUserRecordingConsent(IZoomVideoSDKUser* user) override { + // Called when user gives recording consent + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE STREAM EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* helper, + ZoomVideoSDKLiveStreamStatus status) override { + // Called when live stream status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE TRANSCRIPTION EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override { + // Called when transcription status changes + } + + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override { + // Called when transcription message received + } + + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override { + // Called when original language message received + } + + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage) override { + // Called on transcription error + } + + // ═══════════════════════════════════════════════════════════════════════ + // PHONE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) override { + // Called when phone invite status changes + } + + void onCalloutJoinSuccess(IZoomVideoSDKUser* user, const zchar_t* phoneNumber) override { + // Called when callout user joins + } + + // ═══════════════════════════════════════════════════════════════════════ + // CAMERA CONTROL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCameraControlRequestResult(IZoomVideoSDKUser* user, bool isApproved) override { + // Called when camera control request result + } + + void onCameraControlRequestReceived(IZoomVideoSDKUser* user, + ZoomVideoSDKCameraControlRequestType requestType, + IZoomVideoSDKCameraControlRequestHandler* handler) override { + // Called when camera control request received + } + + // ═══════════════════════════════════════════════════════════════════════ + // REMOTE CONTROL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onRemoteControlStatus(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction, + ZoomVideoSDKRemoteControlStatus status) override { + // Called when remote control status changes + } + + void onRemoteControlRequestReceived(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction, + IZoomVideoSDKRemoteControlRequestHandler* handler) override { + // Called when remote control request received + } + + void onRemoteControlServiceInstallResult(bool bSuccess) override { + // Called when remote control service install result + } + + // ═══════════════════════════════════════════════════════════════════════ + // MULTI-CAMERA EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, + IZoomVideoSDKUser* user, + IZoomVideoSDKRawDataPipe* pipe) override { + // Called when multi-camera stream status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // DEVICE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMicSpeakerVolumeChanged(unsigned int micVolume, + unsigned int speakerVolume) override { + // Called when mic/speaker volume changes + } + + void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, + ZoomVideoSDKAudioDeviceStatus status) override { + // Called when audio device status changes + } + + void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) override { + // Called when test mic status changes + } + + void onSelectedAudioDeviceChanged() override { + // Called when selected audio device changes + } + + void onCameraListChanged() override { + // Called when camera list changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // NETWORK EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, + IZoomVideoSDKUser* user) override { + // Called when video network status changes + } + + void onProxyDetectComplete() override { + // Called when proxy detection completes + } + + void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) override { + // Called when proxy settings notification + } + + void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) override { + // Called when SSL cert verification fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CRC EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) override { + // Called when CRC device status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // ANNOTATION EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) override { + // Called when annotation helper cleanup + } + + void onAnnotationPrivilegeChange(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when annotation privilege changes + } + + void onAnnotationHelperActived(void* handle) override { + // Called when annotation helper activated + } + + // ═══════════════════════════════════════════════════════════════════════ + // FILE TRANSFER EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onSendFileStatus(IZoomVideoSDKSendFile* file, + const FileTransferStatus& status) override { + // Called when send file status changes + } + + void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, + const FileTransferStatus& status) override { + // Called when receive file status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // INCOMING LIVE STREAM EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when bind incoming live stream response + } + + void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when unbind incoming live stream response + } + + void onIncomingLiveStreamStatusResponse(bool bSuccess, + IVideoSDKVector* list) override { + // Called when incoming live stream status response + } + + void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when start incoming live stream response + } + + void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when stop incoming live stream response + } + + // ═══════════════════════════════════════════════════════════════════════ + // SHARE SETTING & CONTENT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onShareSettingChanged(ZoomVideoSDKShareSetting setting) override { + // Called when share settings change + } + + void onUnsharingWindowsChanged(IVideoSDKVector* windowsList, + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override { + // Called when list of unsharing windows changes (macOS only) + } + + void onSharingActiveMonitorChanged(IVideoSDKVector* monitorIDs, + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override { + // Called when active monitors displaying share changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // AUDIO LEVEL & NETWORK EVENTS (NEW) + // ═══════════════════════════════════════════════════════════════════════ + + void onAudioLevelChanged(unsigned int level, bool bAudioSharing, + IZoomVideoSDKUser* pUser) override { + // Called when audio level changes (range 0-9) + } + + void onUserNetworkStatusChanged(ZoomVideoSDKDataType type, + ZoomVideoSDKNetworkStatus level, + IZoomVideoSDKUser* pUser) override { + // Called when network status changes for specific data type + } + + void onUserOverallNetworkStatusChanged(ZoomVideoSDKNetworkStatus level, + IZoomVideoSDKUser* pUser) override { + // Called when overall network status changes + } + + void onShareNetworkStatusChanged(ZoomVideoSDKNetworkStatus shareNetworkStatus, + bool isSendingShare) override { + // Called when share network status changes (deprecated) + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE TRANSCRIPTION EVENTS (ADDITIONAL) + // ═══════════════════════════════════════════════════════════════════════ + + void onSpokenLanguageChanged(ILiveTranscriptionLanguage* spokenLanguage) override { + // Called when spoken language changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // ANNOTATION EVENTS (ADDITIONAL) + // ═══════════════════════════════════════════════════════════════════════ + + void onAnnotationToolTypeChanged(IZoomVideoSDKAnnotationHelper* helper, + void* handle, + ZoomVideoSDKAnnotationToolType toolType) override { + // Called when annotation tool type changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // WHITEBOARD EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser* user, + IZoomVideoSDKWhiteboardHelper* helper) override { + // Called when whiteboard share status changes + } + + void onWhiteboardExported(ZoomVideoSDKExportFormat format, + unsigned char* data, long length) override { + // Called when whiteboard export completes + } + + // ═══════════════════════════════════════════════════════════════════════ + // SUBSESSION (BREAKOUT ROOM) EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onSubSessionStatusChanged(ZoomVideoSDKSubSessionStatus status, + IVideoSDKVector* pSubSessionKitList) override { + // Called when subsession status changes + } + + void onSubSessionManagerHandle(IZoomVideoSDKSubSessionManager* pManager) override { + // Called when user gains subsession manager privilege + } + + void onSubSessionParticipantHandle(IZoomVideoSDKSubSessionParticipant* pParticipant) override { + // Called when user gains/loses subsession participant privileges + } + + void onSubSessionUsersUpdate(ISubSessionKit* pSubSessionKit) override { + // Called when subsession users are updated + } + + void onBroadcastMessageFromMainSession(const zchar_t* sMessage, + const zchar_t* sUserName) override { + // Called when receiving broadcast message from main session + } + + void onSubSessionUserHelpRequest(ISubSessionUserHelpRequestHandler* pHandler) override { + // Called when receiving help request from subsession + } + + void onSubSessionUserHelpRequestResult(ZoomVideoSDKUserHelpRequestResult eResult) override { + // Called with help request result + } + + // ═══════════════════════════════════════════════════════════════════════ + // BROADCAST STREAMING EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onStartBroadcastResponse(bool bSuccess, const zchar_t* channelID) override { + // Called when start broadcast response received + } + + void onStopBroadcastResponse(bool bSuccess) override { + // Called when stop broadcast response received + } + + void onGetBroadcastControlStatus(bool bSuccess, + ZoomVideoSDKBroadcastControlStatus status) override { + // Called when get broadcast status response received + } + + void onStreamingJoinStatusChanged(ZoomVideoSDKStreamingJoinStatus status) override { + // Called when viewer's join status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // RTMS (REAL-TIME MEDIA STREAMS) EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onRealTimeMediaStreamsStatus(RealTimeMediaStreamsStatus status) override { + // Called when RTMS status changes + } + + void onRealTimeMediaStreamsFail(RealTimeMediaStreamsFailReason failReason) override { + // Called when RTMS fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CANVAS SNAPSHOT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCanvasSnapshotTaken(IZoomVideoSDKUser* pUser, bool isShare) override { + // Called when canvas snapshot is taken successfully + } + + void onCanvasSnapshotIncompatible(IZoomVideoSDKUser* pUser) override { + // Called when snapshot cannot be taken due to compatibility + } +}; +``` + +--- + +## Most Important Callbacks + +| Callback | When to Use | +|----------|-------------| +| `onSessionJoin` | Start audio/video, subscribe to self | +| `onSessionLeave` | Cleanup resources | +| `onError` | Handle errors | +| `onUserJoin` | Track new users (but don't subscribe video here!) | +| `onUserLeave` | Cleanup user resources | +| `onUserVideoStatusChanged` | **Subscribe to video here** | +| `onUserAudioStatusChanged` | Track mute/unmute | +| `onChatNewMessageNotify` | Handle chat messages | +| `onUserShareStatusChanged` | Subscribe to screen share | +| `onVideoCanvasSubscribeFail` | Handle subscription failures | + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Event-driven design +- [Video Rendering](../examples/video-rendering.md) - Using video callbacks +- [Build Errors](../troubleshooting/build-errors.md) - Abstract class errors +- [API Reference](windows-reference.md) - Full method signatures + +--- + +**TL;DR**: Copy the template above and implement all methods. Focus on session, user, video, and audio events for basic functionality. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/references/samples.md b/partner-built/zoom-plugin/skills/video-sdk/windows/references/samples.md new file mode 100644 index 00000000..236b2aaa --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/references/samples.md @@ -0,0 +1,282 @@ +# Official Sample Applications + +Reference guide for Zoom Video SDK Windows sample applications. + +**Official Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +--- + +## Sample Overview + +| Sample | Description | Key Features | +|--------|-------------|--------------| +| **VSDK_SkeletonDemo** | Minimal session join | Simplest starting point | +| **VSDK_getRawVideo** | Capture raw video | YUV420 frame extraction | +| **VSDK_getRawAudio** | Capture raw audio | PCM audio extraction | +| **VSDK_getRawShare** | Capture screen share | Share content capture | +| **VSDK_sendRawVideo** | Send custom video | Virtual camera injection | +| **VSDK_sendRawAudio** | Send custom audio | Virtual microphone injection | +| **VSDK_sendRawShare** | Send custom share | Custom screen share source | +| **VSDK_CloudRecording** | Cloud recording | Start/stop cloud recording | +| **VSDK_CommandChannel** | Custom messaging | Send/receive custom commands | +| **VSDK_CallIn** | PSTN dial-in | Phone dial-in support | +| **VSDK_Callout** | PSTN dial-out | Phone dial-out support | +| **VSDK_ServiceQuality** | Network statistics | Quality monitoring | +| **VSDK_TranscriptionAndTranslation** | Live transcription | Real-time captions | +| **VSDK_MultiStreamVideo** | Multiple video streams | Multi-camera support | +| **VSDK_PreviewCameraAndMicrophone** | Device preview | Pre-join device testing | +| **VSDK_Share2ndCameraAsMultiCam** | Secondary camera | Multi-camera sharing | +| **VSDK_Share2ndCameraAsShareScreenDemo** | Camera as share | Camera content sharing | +| **VSDK_ShareScreenPreprocessorDemo** | Share preprocessing | Custom share processing | +| **VSDK_RTMSDemo** | Real-time messaging | RTMS integration | +| **VSDK_DuilibDemo2** | Full UI demo | Complete GUI application | + +--- + +## Recommended Learning Path + +### 1. Start Here: VSDK_SkeletonDemo + +Minimal code to join a session. Demonstrates: +- SDK initialization +- JWT authentication +- Session join/leave +- Windows message loop +- Basic delegate implementation + +**Key patterns to learn:** +```cpp +// 1. Create SDK +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + +// 2. Initialize +ZoomVideoSDKInitParams params; +params.domain = L"https://zoom.us"; +sdk->initialize(params); + +// 3. Add delegate +sdk->addListener(myDelegate); + +// 4. Join session +ZoomVideoSDKSessionContext ctx; +ctx.sessionName = L"session"; +ctx.token = L"jwt"; +ctx.audioOption.connect = false; +sdk->joinSession(ctx); + +// 5. Message loop (CRITICAL) +while (running) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### 2. Video Capture: VSDK_getRawVideo + +Capture raw YUV420 video frames. Demonstrates: +- `IZoomVideoSDKRawDataPipeDelegate` implementation +- `onRawDataFrameReceived()` callback +- YUV buffer extraction (Y, U, V planes) +- Resolution and rotation handling + +**Key patterns:** +```cpp +class VideoCapture : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + // Process frames... + } +}; + +// Subscribe +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +pipe->subscribe(ZoomVideoSDKResolution_720P, videoCapture); +``` + +### 3. Audio Capture: VSDK_getRawAudio + +Capture raw PCM audio. Demonstrates: +- Mixed audio (all participants) +- Per-user audio separation +- Audio format (sample rate, channels) + +**Key patterns:** +```cpp +void onMixedAudioRawDataReceived(AudioRawData* data) override { + char* buffer = data->GetBuffer(); + int length = data->GetBufferLen(); + int sampleRate = data->GetSampleRate(); // 32000 Hz + int channels = data->GetChannelNum(); // 1 or 2 + // Process PCM audio... +} +``` + +### 4. Video Injection: VSDK_sendRawVideo + +Send custom video as virtual camera. Demonstrates: +- `IZoomVideoSDKVideoSource` implementation +- Frame sending with `sendVideoFrame()` +- YUV frame creation +- Frame rate control + +### 5. Audio Injection: VSDK_sendRawAudio + +Send custom audio as virtual microphone. Demonstrates: +- `IZoomVideoSDKVirtualAudioMic` implementation +- PCM audio sending +- Sample rate matching + +--- + +## Sample Categories + +### Raw Data Capture + +| Sample | Input | Output | +|--------|-------|--------| +| VSDK_getRawVideo | Remote video | YUV420 frames | +| VSDK_getRawAudio | Session audio | PCM samples | +| VSDK_getRawShare | Screen share | YUV420 frames | + +### Raw Data Injection + +| Sample | Input | Output | +|--------|-------|--------| +| VSDK_sendRawVideo | YUV420 frames | Virtual camera | +| VSDK_sendRawAudio | PCM samples | Virtual mic | +| VSDK_sendRawShare | YUV420 frames | Screen share | + +### Communication + +| Sample | Feature | +|--------|---------| +| VSDK_CommandChannel | Custom command messaging (60 msgs/sec) | +| VSDK_CallIn | PSTN phone dial-in | +| VSDK_Callout | PSTN phone dial-out | + +### Recording & Streaming + +| Sample | Feature | +|--------|---------| +| VSDK_CloudRecording | Zoom cloud recording | +| VSDK_RTMSDemo | Real-time messaging service | + +### Advanced Features + +| Sample | Feature | +|--------|---------| +| VSDK_ServiceQuality | Network quality statistics | +| VSDK_TranscriptionAndTranslation | Live captions | +| VSDK_MultiStreamVideo | Multiple video streams | +| VSDK_PreviewCameraAndMicrophone | Device preview before join | +| VSDK_ShareScreenPreprocessorDemo | Custom share processing | + +--- + +## Common Patterns Across Samples + +### 1. Initialization Pattern + +All samples follow this pattern: +```cpp +// Initialize SDK +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); +ZoomVideoSDKInitParams params; +params.domain = L"https://zoom.us"; +params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +sdk->initialize(params); +``` + +### 2. Delegate Registration + +Always register before joining: +```cpp +sdk->addListener(new MyDelegate()); +sdk->joinSession(context); +``` + +### 3. Audio Connection + +Connect audio in callback, not during join: +```cpp +context.audioOption.connect = false; // Join config + +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); // Connect here +} +``` + +### 4. Message Loop + +All samples include message loop: +```cpp +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +--- + +## Building Samples + +### Prerequisites + +1. Visual Studio 2019 or 2022 +2. Windows SDK 10.0.19041.0+ +3. Zoom Video SDK (download from Marketplace) + +### Build Steps + +1. Open solution file (`.sln`) +2. Set platform to x64 +3. Set configuration to Release +4. Build solution (Ctrl+Shift+B) +5. Copy SDK DLLs to output directory + +### Configuration + +Each sample uses `config.json`: +```json +{ + "jwt": "your-jwt-token", + "session_name": "test-session", + "password": "", + "user_name": "Bot" +} +``` + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal patterns +- [Session Join Pattern](../examples/session-join-pattern.md) - Complete join code +- [Raw Video Capture](../examples/raw-video-capture.md) - YUV capture details +- [API Reference](windows-reference.md) - Method signatures +- [Delegate Methods](delegate-methods.md) - All callbacks + +--- + +## External Resources + +- **GitHub Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Official Documentation**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Developer Forum**: https://devforum.zoom.us/ + +--- + +**Recommendation**: Start with `VSDK_SkeletonDemo` to understand the basic flow, then move to `VSDK_getRawVideo` or `VSDK_getRawAudio` based on your needs. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/references/windows-reference.md b/partner-built/zoom-plugin/skills/video-sdk/windows/references/windows-reference.md new file mode 100644 index 00000000..910c95a1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/references/windows-reference.md @@ -0,0 +1,1221 @@ +# Zoom Video SDK Windows - API Reference + +**Source**: https://marketplacefront.zoom.us/sdk/custom/windows/ + +--- + +## API Hierarchy (5 Levels Deep) + +Understanding the SDK requires navigating from the singleton entry point through 5 levels of objects. Start from `IZoomVideoSDK` and follow return types. + +### Level 1: Entry Point (Singleton) + +``` +CreateZoomVideoSDKObj() → IZoomVideoSDK* +``` + +| Method | Returns | Purpose | +|--------|---------|---------| +| `initialize(params)` | `ZoomVideoSDKErrors` | Initialize SDK (call once) | +| `joinSession(context)` | `IZoomVideoSDKSession*` | Join and get session object | +| `leaveSession(end)` | `ZoomVideoSDKErrors` | Leave or end session | +| `addListener(delegate)` | `void` | Register event callbacks | +| `getSessionInfo()` | `IZoomVideoSDKSession*` | Get current session | +| `getVideoHelper()` | `IZoomVideoSDKVideoHelper*` | Camera/video control | +| `getAudioHelper()` | `IZoomVideoSDKAudioHelper*` | Mic/speaker control | +| `getShareHelper()` | `IZoomVideoSDKShareHelper*` | Screen sharing | +| `getChatHelper()` | `IZoomVideoSDKChatHelper*` | Chat messaging | +| `getUserHelper()` | `IZoomVideoSDKUserHelper*` | User management | +| `getRecordingHelper()` | `IZoomVideoSDKRecordingHelper*` | Cloud recording | +| `getCmdChannel()` | `IZoomVideoSDKCmdChannel*` | Custom signaling | + +### Level 2: Core Helpers & Session + +#### IZoomVideoSDKSession +```cpp +IZoomVideoSDKSession* session = sdk->getSessionInfo(); +``` + +| Method | Returns | Purpose | +|--------|---------|---------| +| `getMyself()` | `IZoomVideoSDKUser*` | Current user object | +| `getRemoteUsers()` | `IVideoSDKVector*` | All remote users | +| `getSessionName()` | `const zchar_t*` | Session name | +| `getSessionID()` | `const zchar_t*` | Unique session ID | +| `getSessionPassword()` | `const zchar_t*` | Session password | +| `getSessionHost()` | `IZoomVideoSDKUser*` | Host user | + +#### IZoomVideoSDKVideoHelper +Controls YOUR camera. Does NOT control remote users' video. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `startVideo()` | `ZoomVideoSDKErrors` | Turn on your camera | +| `stopVideo()` | `ZoomVideoSDKErrors` | Turn off your camera | +| `rotateMyVideo(rotation)` | `bool` | Rotate camera output | +| `switchCamera(deviceId)` | `bool` | Change camera device | +| `getCameraList()` | `IVideoSDKVector*` | Available cameras | +| `getNumberOfCameras()` | `uint32_t` | Camera count | +| `startVideoCanvasPreview(hwnd, aspect, resolution)` | `ZoomVideoSDKErrors` | Preview your video | +| `stopVideoCanvasPreview(hwnd)` | `ZoomVideoSDKErrors` | Stop preview | + +#### IZoomVideoSDKAudioHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `startAudio()` | `ZoomVideoSDKErrors` | Connect to audio | +| `stopAudio()` | `ZoomVideoSDKErrors` | Disconnect audio | +| `muteAudio(user)` | `ZoomVideoSDKErrors` | Mute a user | +| `unmuteAudio(user)` | `ZoomVideoSDKErrors` | Unmute a user | +| `getMicList()` | `IVideoSDKVector*` | Available mics | +| `getSpeakerList()` | `IVideoSDKVector*` | Available speakers | +| `selectMic(deviceId, name)` | `ZoomVideoSDKErrors` | Select mic | +| `selectSpeaker(deviceId, name)` | `ZoomVideoSDKErrors` | Select speaker | + +#### IZoomVideoSDKShareHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `startShareScreen(monitorId)` | `ZoomVideoSDKErrors` | Share a monitor | +| `startShareWindow(hwnd)` | `ZoomVideoSDKErrors` | Share a window | +| `stopShare()` | `ZoomVideoSDKErrors` | Stop sharing | +| `isShareLocked()` | `bool` | Check if locked | +| `lockShare(lock)` | `ZoomVideoSDKErrors` | Lock sharing | +| `isOtherSharing()` | `bool` | Someone else sharing? | + +### Level 3: User & Rendering Objects + +#### IZoomVideoSDKUser +Represents a participant. Get from `session->getMyself()` or callbacks. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `getUserID()` | `const zchar_t*` | Unique user ID | +| `getUserName()` | `const zchar_t*` | Display name | +| `isHost()` | `bool` | Is session host? | +| `isManager()` | `bool` | Is manager? | +| `GetVideoCanvas()` | `IZoomVideoSDKCanvas*` | **SDK-rendered video** | +| `GetVideoPipe()` | `IZoomVideoSDKRawDataPipe*` | **Raw YUV frames** | +| `GetShareCanvas()` | `IZoomVideoSDKCanvas*` | SDK-rendered share | +| `GetSharePipe()` | `IZoomVideoSDKRawDataPipe*` | Raw share frames | +| `getVideoStatus()` | `ZoomVideoSDKVideoStatus` | Video on/off state | +| `getAudioStatus()` | `ZoomVideoSDKAudioStatus` | Audio mute state | + +#### IZoomVideoSDKCanvas (SDK Rendering) +Let the SDK render video directly to your HWND. **Recommended for most apps.** + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribeWithView(hwnd, aspect, resolution)` | `ZoomVideoSDKErrors` | Start rendering to HWND | +| `unSubscribeWithView(hwnd)` | `ZoomVideoSDKErrors` | Stop rendering | +| `setAspectMode(aspect)` | `ZoomVideoSDKErrors` | Change aspect ratio | +| `setResolution(resolution)` | `ZoomVideoSDKErrors` | Change resolution | + +#### IZoomVideoSDKRawDataPipe (Raw YUV Access) +Get raw YUV420 frames for custom processing. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribe(resolution, delegate)` | `ZoomVideoSDKErrors` | Start receiving frames | +| `unSubscribe(delegate)` | `ZoomVideoSDKErrors` | Stop receiving | +| `getVideoStatus()` | `ZoomVideoSDKVideoStatus` | Check if video is on | + +#### IZoomVideoSDKShareAction +Received in `onUserShareStatusChanged` callback. Controls remote share subscription. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribe()` | `ZoomVideoSDKErrors` | Subscribe to share | +| `unSubscribe()` | `ZoomVideoSDKErrors` | Unsubscribe | +| `subscribeWithView(hwnd, aspect)` | `ZoomVideoSDKErrors` | Render share to HWND | +| `unSubscribeWithView(hwnd)` | `ZoomVideoSDKErrors` | Stop rendering | +| `getShareCanvas()` | `IZoomVideoSDKCanvas*` | Get share canvas | +| `getSharePipe()` | `IZoomVideoSDKRawDataPipe*` | Get raw share pipe | +| `getShareType()` | `ZoomVideoSDKShareType` | Screen/window/etc | + +### Level 4: Devices, Chat & Callbacks + +#### IZoomVideoSDKCameraDevice +| Method | Returns | +|--------|---------| +| `getDeviceId()` | `const zchar_t*` | +| `getDeviceName()` | `const zchar_t*` | +| `isSelectedDevice()` | `bool` | + +#### IZoomVideoSDKMicDevice / IZoomVideoSDKSpeakerDevice +| Method | Returns | +|--------|---------| +| `getDeviceId()` | `const zchar_t*` | +| `getDeviceName()` | `const zchar_t*` | +| `isSelectedDevice()` | `bool` | + +#### IZoomVideoSDKChatHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `sendChatToAll(message)` | `ZoomVideoSDKErrors` | Broadcast message | +| `sendChatToUser(user, message)` | `ZoomVideoSDKErrors` | Private message | +| `canChatMessageBeDeleted(msgId)` | `bool` | Check delete permission | +| `deleteChatMessage(msgId)` | `ZoomVideoSDKErrors` | Delete a message | + +#### IZoomVideoSDKChatMessage +Received in `onChatNewMessageNotify` callback. + +| Method | Returns | +|--------|---------| +| `getMessageID()` | `const zchar_t*` | +| `getSendUser()` | `IZoomVideoSDKUser*` | +| `getReceiverUser()` | `IZoomVideoSDKUser*` | +| `getContent()` | `const zchar_t*` | +| `getTimeStamp()` | `time_t` | +| `isChatToAll()` | `bool` | +| `isSelfSend()` | `bool` | + +#### IZoomVideoSDKRawDataPipeDelegate +Implement this to receive raw YUV frames. + +```cpp +class MyVideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Process frame + } + void onRawDataStatusChanged(RawDataStatus status) override { + // Handle on/off + } +}; +``` + +### Level 5: Raw Data & Utilities + +#### YUVRawDataI420 +Video frame in YUV420 format (I420). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `GetYBuffer()` | `char*` | Y plane (luminance) | +| `GetUBuffer()` | `char*` | U plane (chrominance) | +| `GetVBuffer()` | `char*` | V plane (chrominance) | +| `GetStreamWidth()` | `unsigned int` | Frame width | +| `GetStreamHeight()` | `unsigned int` | Frame height | +| `GetRotation()` | `unsigned int` | 0, 90, 180, 270 | +| `GetTimeStamp()` | `unsigned long long` | Frame timestamp | +| `CanAddRef()` / `AddRef()` / `Release()` | - | Reference counting | + +#### AudioRawData +PCM audio samples (16-bit signed). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `GetBuffer()` | `char*` | PCM sample buffer | +| `GetBufferLen()` | `unsigned int` | Buffer size (bytes) | +| `GetSampleRate()` | `unsigned int` | Sample rate (Hz) | +| `GetChannelNum()` | `unsigned int` | 1=mono, 2=stereo | + +#### IZoomVideoSDKUserHelper +Admin actions on users. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `removeUser(user)` | `bool` | Kick user from session | +| `makeHost(user)` | `bool` | Transfer host role | +| `makeManager(user)` | `bool` | Promote to manager | +| `revokeManager(user)` | `bool` | Demote manager | +| `changeName(user, name)` | `bool` | Rename user | + +#### IZoomVideoSDKCmdChannel +Custom signaling (max 60 messages/second). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `sendCommand(user, command)` | `ZoomVideoSDKErrors` | Send to one user | +| `sendCommandToAll(command)` | `ZoomVideoSDKErrors` | Broadcast to all | + +#### IVirtualBackgroundItem +| Method | Returns | +|--------|---------| +| `getImageFilePath()` | `const zchar_t*` | +| `getImageName()` | `const zchar_t*` | +| `getType()` | `ZoomVideoSDKVirtualBackgroundDataType` | +| `isSelected()` | `bool` | + +--- + +## Critical Timing Rules + +### ⚠️ CRITICAL: Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +**WRONG** (causes Error 2 - Internal_Error): +```cpp +void onUserJoin(IZoomVideoSDKUserHelper* helper, IVideoSDKVector* userList) { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + // ERROR: Video may not be ready yet! + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } +} +``` + +**CORRECT**: +```cpp +void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) { + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user == myself) continue; // Skip self + + // Check if video is actually on + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (pipe) { + ZoomVideoSDKVideoStatus status = pipe->getVideoStatus(); + if (status.isOn) { + // NOW it's safe to subscribe + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + canvas->subscribeWithView(hwnd, aspect, resolution); + } + } + } +} +``` + +### Video Status Structure + +```cpp +struct ZoomVideoSDKVideoStatus { + bool isOn; // true = video is transmitting + bool hasSource; // true = camera is available +}; +``` + +- `isOn == true` → Safe to subscribe +- `isOn == false` → Unsubscribe or skip +- `hasSource == false` → User has no camera + +### Subscribe Fail Reasons (onVideoCanvasSubscribeFail) + +```cpp +enum ZoomVideoSDKSubscribeFailReason { + ZoomVideoSDKSubscribeFailReason_None = 0, + ZoomVideoSDKSubscribeFailReason_HasSubscribe1080POr720P = 1, + ZoomVideoSDKSubscribeFailReason_HasSubscribeTwo720P = 2, + ZoomVideoSDKSubscribeFailReason_HasSubscribeExceededLimit = 3, + ZoomVideoSDKSubscribeFailReason_HasSubscribeTwoShare = 4, + ZoomVideoSDKSubscribeFailReason_HasSubscribeVideo1080POr720PAndOneShare = 5, + ZoomVideoSDKSubscribeFailReason_TooFrequentCall = 6 +}; +``` + +**Handling TooFrequentCall**: Add `Sleep(200)` between subscribe calls. + +### SDK Error Codes Quick Reference + +| Code | Name | Common Cause | +|------|------|--------------| +| 0 | Success | - | +| 1 | Wrong_Usage | Calling method in wrong state | +| 2 | Internal_Error | Video not ready, subscribe too early | +| 7 | Invalid_Parameter | NULL pointer, bad HWND | +| 8 | Call_Too_Frequently | Need Sleep() between calls | + +--- + +## Two Rendering Approaches + +| Approach | Interface | When to Use | +|----------|-----------|-------------| +| **Canvas API** | `IZoomVideoSDKCanvas::subscribeWithView(HWND)` | Standard apps, best quality | +| **Raw Data Pipe** | `IZoomVideoSDKRawDataPipe::subscribe(delegate)` | Custom processing, effects, recording | + +### Canvas API (Recommended) +```cpp +// SDK renders directly to your window - no YUV conversion needed +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +canvas->subscribeWithView(hwnd, ZoomVideoSDKVideoAspect_PanAndScan, ZoomVideoSDKResolution_Auto); +``` + +### Raw Data Pipe (Advanced) +```cpp +// You receive YUV420 frames and must render them yourself +class MyRenderer : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Convert YUV to RGB, then render with GDI/DirectX/OpenGL + } +}; + +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +pipe->subscribe(ZoomVideoSDKResolution_720P, myRenderer); +``` + +--- + +## Complete Class List + +### Core SDK + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDK` | Main singleton object - session creation, callbacks, features | +| `IZoomVideoSDKSession` | Session information interface | +| `IZoomVideoSDKDelegate` | Event callbacks for session events | +| `IZoomVideoSDKUser` | User object interface | +| `IZoomVideoSDKUserHelper` | User management helper | + +### Raw Data Interfaces + +| Class | Description | +|-------|-------------| +| `AudioRawData` | Audio raw data handler (PCM 16-bit) | +| `YUVRawDataI420` | YUV raw data handler (I420 format) | +| `YUVProcessDataI420` | YUV processing data | +| `IZoomVideoSDKRawDataPipe` | Video/share raw data pipe | +| `IZoomVideoSDKRawDataPipeDelegate` | Video/share raw data sink | +| `IYUVRawDataI420Converter` | I420 YUV converter | + +### Virtual Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKVirtualAudioMic` | Virtual audio microphone for injection | +| `IZoomVideoSDKVirtualAudioSpeaker` | Virtual audio speaker | +| `IZoomVideoSDKVideoSource` | Video source for injection | +| `IZoomVideoSDKVideoSourcePreProcessor` | Video preprocessing | +| `IZoomVideoSDKShareSource` | Share source for injection | +| `IZoomVideoSDKSharePreprocessor` | Share preprocessing | + +### Senders + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSender` | Audio raw data sender | +| `IZoomVideoSDKVideoSender` | Video raw data sender | +| `IZoomVideoSDKShareSender` | Share raw data sender | +| `IZoomVideoSDKShareAudioSender` | Share audio sender | +| `IZoomVideoSDKShareAudioSource` | Share audio source | + +### Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioHelper` | Audio controls | +| `IZoomVideoSDKVideoHelper` | Video/camera controls | +| `IZoomVideoSDKShareHelper` | Screen sharing | +| `IZoomVideoSDKChatHelper` | Chat messaging | +| `IZoomVideoSDKRecordingHelper` | Cloud recording | +| `IZoomVideoSDKLiveStreamHelper` | RTMP live streaming | +| `IZoomVideoSDKLiveTranscriptionHelper` | Live transcription | +| `IZoomVideoSDKPhoneHelper` | Phone dial-out | +| `IZoomVideoSDKCmdChannel` | Command channel | +| `IZoomVideoSDKCRCHelper` | CRC helper | +| `IZoomVideoSDKWhiteboardHelper` | Whiteboard | +| `IZoomVideoSDKAnnotationHelper` | Annotations | +| `IZoomVideoSDKNetworkConnectionHelper` | Network connection | +| `IZoomVideoSDKSubSessionHelper` | Subsession helper | +| `IZoomVideoSDKSubSessionManager` | Subsession manager | +| `IZoomVideoSDKRTMSHelper` | Real-time media streams | +| `IZoomVideoSDKIncomingLiveStreamHelper` | Incoming live stream | + +### Settings Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSettingHelper` | Audio settings | +| `IZoomVideoSDKVideoSettingHelper` | Video settings | +| `IZoomVideoSDKShareSettingHelper` | Share settings | +| `IZoomVideoSDKTestAudioDeviceHelper` | Audio device testing | + +### Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCameraDevice` | Camera device | +| `IZoomVideoSDKMicDevice` | Microphone device | +| `IZoomVideoSDKSpeakerDevice` | Speaker device | +| `IVirtualBackgroundItem` | Virtual background item | + +### Streaming + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKBroadcastStreamingController` | Broadcast controller | +| `IZoomVideoSDKBroadcastStreamingViewer` | Broadcast viewer | +| `IZoomVideoSDKBroadcastStreamingAudioCallback` | Broadcast audio callback | +| `IZoomVideoSDKBroadcastStreamingVideoCallback` | Broadcast video callback | + +### Session & Messages + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKChatMessage` | Chat message | +| `ILiveTranscriptionLanguage` | Transcription language | +| `ILiveTranscriptionMessageInfo` | Transcription message | +| `IZoomVideoSDKSessionDialInNumberInfo` | Dial-in info | +| `IZoomVideoSDKPhoneSupportCountryInfo` | Phone country info | + +### Subsessions + +| Class | Description | +|-------|-------------| +| `ISubSessionKit` | Subsession kit | +| `ISubSessionUser` | Subsession user | +| `ISubSessionUserHelpRequestHandler` | Help request handler | +| `IZoomVideoSDKSubSessionParticipant` | Subsession participant | + +### File Transfer + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKFileTransferBaseInfo` | File transfer base info | +| `IZoomVideoSDKSendFile` | Send file interface | +| `IZoomVideoSDKReceiveFile` | Receive file interface | + +### Handlers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKPasswordHandler` | Password handler | +| `IZoomVideoSDKRecordingConsentHandler` | Recording consent | +| `IZoomVideoSDKCameraControlRequestHandler` | Camera control requests | +| `IZoomVideoSDKRemoteCameraControlHelper` | Remote camera control | +| `IZoomVideoSDKProxySettingHandler` | Proxy settings | +| `IZoomVideoSDKSSLCertificateInfo` | SSL certificate info | + +### Canvas & Actions + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCanvas` | Video/share canvas | +| `IZoomVideoSDKShareAction` | Share action | +| `IMonitorListBuilder` | Monitor list builder | + +### Utilities + +| Class | Description | +|-------|-------------| +| `IVideoSDKVector` | SDK vector collection | + +--- + +## Structures + +### Initialization + +```cpp +struct ZoomVideoSDKInitParams { + const zchar_t* domain; // Required: L"https://zoom.us" + bool enableLog; // Enable logging + const zchar_t* logFilePrefix; // Log file prefix + ZoomVideoSDKRawDataMemoryMode videoRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode shareRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode audioRawDataMemoryMode; + bool enableIndirectRawdata; // Indirect raw data access + ZoomVideoSDKExtendParams* extendParams; // Extended parameters +}; + +struct ZoomVideoSDKExtendParams { + const zchar_t* speakerTestFilePath; + // Additional extended parameters +}; +``` + +### Session Context + +```cpp +struct ZoomVideoSDKSessionContext { + const zchar_t* sessionName; // Required + const zchar_t* sessionPassword; // Optional + const zchar_t* userName; // Required + const zchar_t* token; // Required: JWT token + unsigned int sessionIdleTimeoutMins; // 0 = never timeout, default 40 + bool autoLoadMutliStream; // Auto-load multiple streams + + ZoomVideoSDKVideoOption videoOption; + ZoomVideoSDKAudioOption audioOption; + + IZoomVideoSDKVideoSource* externalVideoSource; + IZoomVideoSDKVirtualAudioMic* virtualAudioMic; + IZoomVideoSDKVirtualAudioSpeaker* virtualAudioSpeaker; + IZoomVideoSDKVideoSourcePreProcessor* preProcessor; +}; + +struct ZoomVideoSDKVideoOption { + bool localVideoOn; // Start with video on +}; + +struct ZoomVideoSDKAudioOption { + bool connect; // Connect to audio + bool mute; // Start muted +}; +``` + +### Statistics + +```cpp +struct ZoomVideoSDKSessionAudioStatisticInfo { + int frequency; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +struct ZoomVideoSDKSessionASVStatisticInfo { + int frame_width; + int frame_height; + int fps; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +// Aliases +typedef ZoomVideoSDKSessionASVStatisticInfo _SessionASVStatisticInfo; +typedef ZoomVideoSDKSessionAudioStatisticInfo _SessionAudioStatisticInfo; + +struct ZoomVideoSDKVideoStatisticInfo { + int width; + int height; + int fps; + int bps; +}; + +struct ZoomVideoSDKShareStatisticInfo { + int width; + int height; + int fps; + int bps; +}; +``` + +### Video/Audio Status + +```cpp +struct ZoomVideoSDKVideoStatus { + bool isOn; + bool hasSource; +}; + +struct ZoomVideoSDKAudioStatus { + bool isMuted; + bool isAudioConnected; + ZoomVideoSDKAudioType audioType; +}; + +struct VideoSourceCapability { + unsigned int width; + unsigned int height; + unsigned int frame; // FPS +}; +``` + +### Live Streaming + +```cpp +struct ZoomVideoSDKLiveStreamParams { + const zchar_t* streamUrl; // RTMP URL + const zchar_t* streamKey; // Stream key + const zchar_t* broadcastUrl; // Broadcast URL +}; + +struct ZoomVideoSDKLiveStreamSetting { + // Live stream settings +}; + +struct IncomingLiveStreamStatus { + // Incoming stream status +}; +``` + +### Share + +```cpp +struct ZoomVideoSDKShareOption { + bool isWithDeviceAudio; + bool isOptimizeForSharedVideo; +}; + +struct ZoomVideoSDKShareCursorData { + int x; + int y; + // Cursor information +}; + +struct ZoomVideoSDKSharePreprocessParam { + // Preprocessing parameters +}; +``` + +### File Transfer + +```cpp +struct FileTransferProgress { + unsigned long long transferredSize; + unsigned long long totalSize; + float percentage; +}; + +struct ZoomVideoSDKFileStatus { + // File transfer status +}; +``` + +### Misc + +```cpp +struct ZoomVideoSDKViewSize { + int width; + int height; +}; + +struct tagVideoPreferenceSetting { + // Video preference settings +}; + +struct tagProxySettings { + // Proxy configuration +}; + +struct InvitePhoneUserInfo { + const zchar_t* countryCode; + const zchar_t* phoneNumber; + const zchar_t* displayName; +}; + +struct ZoomVideoSDKSteamingJoinContext { + // Streaming join context +}; +``` + +--- + +## IZoomVideoSDK (Main Interface) + +```cpp +class IZoomVideoSDK { +public: + // Lifecycle + virtual ZoomVideoSDKErrors initialize(ZoomVideoSDKInitParams& params) = 0; + virtual ZoomVideoSDKErrors cleanup() = 0; + + // Session management + virtual IZoomVideoSDKSession* joinSession(ZoomVideoSDKSessionContext& params) = 0; + virtual ZoomVideoSDKErrors leaveSession(bool end) = 0; + virtual IZoomVideoSDKSession* getSessionInfo() = 0; + virtual bool isInSession() = 0; + + // Listeners + virtual void addListener(IZoomVideoSDKDelegate* listener) = 0; + virtual void removeListener(IZoomVideoSDKDelegate* listener) = 0; + + // Helpers + virtual IZoomVideoSDKAudioHelper* getAudioHelper() = 0; + virtual IZoomVideoSDKVideoHelper* getVideoHelper() = 0; + virtual IZoomVideoSDKUserHelper* getUserHelper() = 0; + virtual IZoomVideoSDKShareHelper* getShareHelper() = 0; + virtual IZoomVideoSDKRecordingHelper* getRecordingHelper() = 0; + virtual IZoomVideoSDKLiveStreamHelper* getLiveStreamHelper() = 0; + virtual IZoomVideoSDKChatHelper* getChatHelper() = 0; + virtual IZoomVideoSDKCmdChannel* getCmdChannel() = 0; + virtual IZoomVideoSDKPhoneHelper* getPhoneHelper() = 0; + virtual IZoomVideoSDKLiveTranscriptionHelper* getLiveTranscriptionHelper() = 0; + virtual IZoomVideoSDKCRCHelper* getCRCHelper() = 0; + virtual IZoomVideoSDKWhiteboardHelper* getWhiteboardHelper() = 0; + virtual IZoomVideoSDKSubSessionHelper* getSubSessionHelper() = 0; + virtual IZoomVideoSDKRTMSHelper* getRealTimeMediaStreamsHelper() = 0; + virtual IZoomVideoSDKIncomingLiveStreamHelper* getIncomingLiveStreamHelper() = 0; + + // Settings + virtual IZoomVideoSDKAudioSettingHelper* getAudioSettingHelper() = 0; + virtual IZoomVideoSDKVideoSettingHelper* getVideoSettingHelper() = 0; + virtual IZoomVideoSDKShareSettingHelper* getShareSettingHelper() = 0; + virtual IZoomVideoSDKTestAudioDeviceHelper* GetAudioDeviceTestHelper() = 0; + virtual IZoomVideoSDKNetworkConnectionHelper* getNetworkConnectionHelper() = 0; + + // Utilities + virtual const zchar_t* getSDKVersion() = 0; + virtual const zchar_t* exportLog() = 0; + virtual ZoomVideoSDKErrors cleanAllExportedLogs() = 0; +}; +``` + +### Factory Functions + +```cpp +// Create SDK object +IZoomVideoSDK* CreateZoomVideoSDKObj(); + +// Destroy SDK object +void DestroyZoomVideoSDKObj(); +``` + +--- + +## IZoomVideoSDKDelegate (Callbacks) + +```cpp +class IZoomVideoSDKDelegate { +public: + // Session callbacks + virtual void onSessionJoin() = 0; + virtual void onSessionLeave() = 0; + virtual void onSessionLeave(ZoomVideoSDKSessionLeaveReason eReason) = 0; + virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) = 0; + virtual void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) = 0; + virtual void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) = 0; + + // User callbacks + virtual void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserLeave(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserHostChanged(IZoomVideoSDKUserHelper* pUserHelper, + IZoomVideoSDKUser* pUser) = 0; + virtual void onUserManagerChanged(IZoomVideoSDKUser* pUser) = 0; + virtual void onUserNameChanged(IZoomVideoSDKUser* pUser) = 0; + + // Video callbacks + virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) = 0; + virtual void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) = 0; + + // Audio callbacks + virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* list) = 0; + + // Raw audio callbacks + virtual void onMixedAudioRawDataReceived(AudioRawData* data_) = 0; + virtual void onOneWayAudioRawDataReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onSharedAudioRawDataReceived(AudioRawData* data_) = 0; + + // Share callbacks + virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onShareContentChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onFailedToStartShare(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser) = 0; + virtual void onShareContentSizeChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + + // Chat callbacks + virtual void onChatNewMessageNotify(IZoomVideoSDKChatHelper* pChatHelper, + IZoomVideoSDKChatMessage* messageItem) = 0; + virtual void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* pChatHelper, + const zchar_t* msgID, + ZoomVideoSDKChatMessageDeleteType deleteBy) = 0; + virtual void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* pChatHelper, + ZoomVideoSDKChatPrivilegeType privilege) = 0; + + // Command channel callbacks + virtual void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) = 0; + virtual void onCommandChannelConnectResult(bool isSuccess) = 0; + + // Recording callbacks + virtual void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* pHandler) = 0; + virtual void onUserRecordingConsent(IZoomVideoSDKUser* pUser) = 0; + virtual void onHostAskUnmute() = 0; + + // Live stream callbacks + virtual void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* pLiveStreamHelper, + ZoomVideoSDKLiveStreamStatus status) = 0; + + // Live transcription callbacks + virtual void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) = 0; + virtual void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + virtual void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + virtual void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage) = 0; + + // Phone callbacks + virtual void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) = 0; + virtual void onCalloutJoinSuccess(IZoomVideoSDKUser* pUser, const zchar_t* phoneNumber) = 0; + + // Camera control callbacks + virtual void onCameraControlRequestResult(IZoomVideoSDKUser* pUser, bool isApproved) = 0; + virtual void onCameraControlRequestReceived(IZoomVideoSDKUser* pUser, + ZoomVideoSDKCameraControlRequestType requestType, + IZoomVideoSDKCameraControlRequestHandler* handler) = 0; + + // Remote control callbacks + virtual void onRemoteControlStatus(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction, + ZoomVideoSDKRemoteControlStatus status) = 0; + virtual void onRemoteControlRequestReceived(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction, + IZoomVideoSDKRemoteControlRequestHandler* handler) = 0; + virtual void onRemoteControlServiceInstallResult(bool bSuccess) = 0; + + // Multi-camera callbacks + virtual void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKRawDataPipe* pVideoPipe) = 0; + + // Device callbacks + virtual void onMicSpeakerVolumeChanged(unsigned int micVolume, + unsigned int speakerVolume) = 0; + virtual void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, + ZoomVideoSDKAudioDeviceStatus status) = 0; + virtual void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) = 0; + virtual void onSelectedAudioDeviceChanged() = 0; + virtual void onCameraListChanged() = 0; + + // Network callbacks + virtual void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, + IZoomVideoSDKUser* pUser) = 0; + virtual void onProxyDetectComplete() = 0; + virtual void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) = 0; + virtual void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) = 0; + + // CRC callbacks + virtual void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) = 0; + + // Canvas callbacks + virtual void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason fail_reason, + IZoomVideoSDKUser* pUser, void* handle) = 0; + virtual void onShareCanvasSubscribeFail(IZoomVideoSDKUser* pUser, void* handle, + IZoomVideoSDKShareAction* pShareAction) = 0; + + // Annotation callbacks + virtual void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) = 0; + virtual void onAnnotationPrivilegeChange(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onAnnotationHelperActived(void* handle) = 0; + + // File transfer callbacks + virtual void onSendFileStatus(IZoomVideoSDKSendFile* file, + const FileTransferStatus& status) = 0; + virtual void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, + const FileTransferStatus& status) = 0; + + // Misc callbacks + virtual void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) = 0; + + // Incoming live stream callbacks + virtual void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onIncomingLiveStreamStatusResponse(bool bSuccess, + IVideoSDKVector* list) = 0; + virtual void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; +}; +``` + +--- + +## Raw Data Interfaces + +### AudioRawData + +```cpp +class AudioRawData { +public: + virtual char* GetBuffer() = 0; // PCM 16-bit buffer + virtual unsigned int GetBufferLen() = 0; // Buffer length in bytes + virtual unsigned int GetSampleRate() = 0;// Sample rate (Hz) + virtual unsigned int GetChannelNum() = 0;// Channels (1=mono, 2=stereo) + virtual unsigned long long GetTimeStamp() = 0; +}; +``` + +### YUVRawDataI420 + +```cpp +class YUVRawDataI420 { +public: + virtual char* GetYBuffer() = 0; + virtual char* GetUBuffer() = 0; + virtual char* GetVBuffer() = 0; + virtual char* GetAlphaBuffer() = 0; // Optional alpha channel + virtual char* GetBuffer() = 0; // Full YUV buffer + virtual unsigned int GetBufferLen() = 0; + virtual unsigned int GetStreamWidth() = 0; + virtual unsigned int GetStreamHeight() = 0; + virtual unsigned int GetRotation() = 0; // 0, 90, 180, 270 + virtual unsigned long long GetSourceID() = 0; + virtual unsigned long long GetTimeStamp() = 0; + virtual bool IsLimitedI420() = 0; + + // Reference counting + virtual bool CanAddRef() = 0; + virtual bool AddRef() = 0; + virtual int Release() = 0; +}; +``` + +### IZoomVideoSDKRawDataPipeDelegate + +```cpp +class IZoomVideoSDKRawDataPipeDelegate { +public: + virtual void onRawDataFrameReceived(YUVRawDataI420* data) = 0; + virtual void onRawDataStatusChanged(RawDataStatus status) = 0; +}; + +enum RawDataStatus { + RawData_On, + RawData_Off +}; +``` + +--- + +## Virtual Device Interfaces + +### IZoomVideoSDKVirtualAudioMic + +```cpp +class IZoomVideoSDKVirtualAudioMic { +public: + virtual void onMicInitialize(IZoomVideoSDKAudioSender* sender) = 0; + virtual void onMicStartSend() = 0; + virtual void onMicStopSend() = 0; + virtual void onMicUninitialized() = 0; +}; + +class IZoomVideoSDKAudioSender { +public: + virtual ZoomVideoSDKErrors Send(char* data, + unsigned int dataLength, + int sampleRate) = 0; +}; +``` + +### IZoomVideoSDKVirtualAudioSpeaker + +```cpp +class IZoomVideoSDKVirtualAudioSpeaker { +public: + virtual void onVirtualSpeakerMixedAudioReceived(AudioRawData* data_) = 0; + virtual void onVirtualSpeakerOneWayAudioReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onVirtualSpeakerSharedAudioReceived(AudioRawData* data_) = 0; +}; +``` + +### IZoomVideoSDKVideoSource + +```cpp +class IZoomVideoSDKVideoSource { +public: + virtual void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* supportCapList, + VideoSourceCapability& suggestCap) = 0; + virtual void onPropertyChange(IVideoSDKVector* supportCapList, + VideoSourceCapability suggestCap) = 0; + virtual void onStartSend() = 0; + virtual void onStopSend() = 0; + virtual void onUninitialized() = 0; +}; + +class IZoomVideoSDKVideoSender { +public: + virtual ZoomVideoSDKErrors sendVideoFrame(char* frameBuffer, + int width, int height, + int frameLength, + int rotation) = 0; + // Alternative with Y, U, V planes + virtual ZoomVideoSDKErrors sendVideoFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +### IZoomVideoSDKShareSource + +```cpp +class IZoomVideoSDKShareSource { +public: + virtual void onShareSendStarted(IZoomVideoSDKShareSender* pSender) = 0; + virtual void onShareSendStopped() = 0; +}; + +class IZoomVideoSDKShareSender { +public: + virtual ZoomVideoSDKErrors sendShareFrame(char* frameBuffer, + int width, int height, + int frameLength) = 0; + virtual ZoomVideoSDKErrors sendShareFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +--- + +## Enumerations + +### ZoomVideoSDKErrors + +```cpp +enum ZoomVideoSDKErrors { + ZoomVideoSDKErrors_Success = 0, + ZoomVideoSDKErrors_Wrong_Usage = 1, + ZoomVideoSDKErrors_Internal_Error = 2, + ZoomVideoSDKErrors_Uninitialize = 3, + ZoomVideoSDKErrors_Memory_Error = 4, + ZoomVideoSDKErrors_Load_Module_Error = 5, + ZoomVideoSDKErrors_UnLoad_Module_Error = 6, + ZoomVideoSDKErrors_Invalid_Parameter = 7, + ZoomVideoSDKErrors_Call_Too_Frequently = 8, + ZoomVideoSDKErrors_No_Impl = 9, + ZoomVideoSDKErrors_Dont_Support_Feature = 10, + ZoomVideoSDKErrors_Unknown = 100, + + // Auth errors (1000+) + ZoomVideoSDKErrors_Auth_Error = 1001, + ZoomVideoSDKErrors_Auth_Empty_Key_or_Secret = 1002, + ZoomVideoSDKErrors_Auth_Wrong_Key_or_Secret = 1003, + ZoomVideoSDKErrors_Auth_DoesNot_Support_SDK = 1004, + ZoomVideoSDKErrors_Auth_Disable_SDK = 1005, + + // Session errors (3000+) + ZoomVideoSDKErrors_Session_Join_Failed = 3001, + ZoomVideoSDKErrors_Session_No_Rights = 3002, + ZoomVideoSDKErrors_Session_Already_In_Progress = 3003, + ZoomVideoSDKErrors_Session_Dont_Support_SessionType = 3004, + ZoomVideoSDKErrors_Session_Reconnecting = 3005, + ZoomVideoSDKErrors_Session_Disconnecting = 3006, + ZoomVideoSDKErrors_Session_Not_Started = 3007, + ZoomVideoSDKErrors_Session_Need_Password = 3008, + ZoomVideoSDKErrors_Session_Password_Wrong = 3009, + ZoomVideoSDKErrors_Session_Remote_DB_Error = 3010, + ZoomVideoSDKErrors_Session_Invalid_Param = 3011, + + // Audio/Video errors + ZoomVideoSDKErrors_Session_Audio_Error = 4001, + ZoomVideoSDKErrors_Session_Audio_No_Microphone = 4002, + ZoomVideoSDKErrors_Session_Video_Error = 5001, + ZoomVideoSDKErrors_Session_Video_Device_Error = 5002, + + // Share errors + ZoomVideoSDKErrors_Session_Share_Error = 6001, + ZoomVideoSDKErrors_Session_Share_Module_Not_Ready = 6002, + ZoomVideoSDKErrors_Session_Share_You_Are_Not_Sharing = 6003, + ZoomVideoSDKErrors_Session_Share_Type_Is_Not_Support = 6004, + ZoomVideoSDKErrors_Session_Share_Internal_Error = 6005, + + ZoomVideoSDKErrors_Dont_Support_Multi_Stream_Video_User = 7001, +}; +``` + +### Resolution Options + +```cpp +enum ZoomVideoSDKResolution { + ZoomVideoSDKResolution_90P, + ZoomVideoSDKResolution_180P, + ZoomVideoSDKResolution_360P, + ZoomVideoSDKResolution_720P, + ZoomVideoSDKResolution_1080P +}; +``` + +### Recording Status + +```cpp +enum RecordingStatus { + Recording_Start, + Recording_Stop, + Recording_Pause, + Recording_Connecting, + Recording_DiskFull +}; +``` + +### Memory Mode + +```cpp +enum ZoomVideoSDKRawDataMemoryMode { + ZoomVideoSDKRawDataMemoryModeStack, + ZoomVideoSDKRawDataMemoryModeHeap +}; +``` + +### Session Leave Reason + +```cpp +enum ZoomVideoSDKSessionLeaveReason { + ZoomVideoSDKSessionLeaveReason_EndByHost, + ZoomVideoSDKSessionLeaveReason_HostEndForAll, + ZoomVideoSDKSessionLeaveReason_KickedByHost, + ZoomVideoSDKSessionLeaveReason_Timeout, + ZoomVideoSDKSessionLeaveReason_SessionIdleTimeout, + ZoomVideoSDKSessionLeaveReason_Default +}; +``` + +### Live Transcription Status + +```cpp +enum ZoomVideoSDKLiveTranscriptionStatus { + ZoomVideoSDKLiveTranscription_Status_Stop, + ZoomVideoSDKLiveTranscription_Status_Start +}; +``` + +### Network Status + +```cpp +enum ZoomVideoSDKNetworkStatus { + ZoomVideoSDKNetworkStatus_Good, + ZoomVideoSDKNetworkStatus_Normal, + ZoomVideoSDKNetworkStatus_Poor, + ZoomVideoSDKNetworkStatus_Bad, + ZoomVideoSDKNetworkStatus_Connecting +}; +``` + +--- + +## Essential Headers + +```cpp +// Core headers +#include "zoom_video_sdk_api.h" // CreateZoomVideoSDKObj, DestroyZoomVideoSDKObj +#include "zoom_video_sdk_interface.h" // IZoomVideoSDK +#include "zoom_video_sdk_delegate_interface.h" // IZoomVideoSDKDelegate +#include "zoom_video_sdk_def.h" // Structures, enums +#include "zoom_video_sdk_platform.h" // Platform definitions +#include "zoom_sdk_raw_data_def.h" // Raw data types + +// Helper headers +#include "helpers/zoom_video_sdk_user_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_helper_interface.h" +#include "helpers/zoom_video_sdk_video_helper_interface.h" +#include "helpers/zoom_video_sdk_share_helper_interface.h" +#include "helpers/zoom_video_sdk_chat_helper_interface.h" +#include "helpers/zoom_video_sdk_recording_helper_interface.h" +#include "helpers/zoom_video_sdk_livestream_helper_interface.h" +#include "helpers/zoom_video_sdk_livetranscription_helper_interface.h" +#include "helpers/zoom_video_sdk_cmd_channel_interface.h" +#include "helpers/zoom_video_sdk_phone_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_send_rawdata_interface.h" +#include "helpers/zoom_video_sdk_video_source_helper_interface.h" + +// Message interfaces +#include "zoom_video_sdk_chat_message_interface.h" +#include "zoom_video_sdk_session_info_interface.h" + +// Namespace +using namespace ZOOM_VIDEO_SDK_NAMESPACE; +// Or use macro +USING_ZOOM_VIDEO_SDK_NAMESPACE +``` + +--- + +## Additional Resources + +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Sample Code**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Dev Forum**: https://devforum.zoom.us +- **C# .NET Sample**: https://github.com/zoom/videosdk-windows-dotnet-quickstart diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/build-errors.md b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/build-errors.md new file mode 100644 index 00000000..3ed96007 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/build-errors.md @@ -0,0 +1,321 @@ +# Build Errors Guide + +Common build errors when working with the Zoom Video SDK for Windows and how to fix them. + +--- + +## Include Order (CRITICAL) + +SDK headers have dependency issues. **Include in this exact order**: + +```cpp +#include // MUST be first +#include // MUST be second (for uint32_t) + +// Standard library +#include +#include +#include +#include +#include +#include + +// SDK headers +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" +#include "zoom_sdk_raw_data_def.h" // For YUVRawDataI420 +``` + +--- + +## Error: 'uint32_t' is not a member of 'std' + +### Symptom + +``` +error C2039: 'uint32_t': is not a member of 'std' +``` + +### Cause + +SDK headers use `uint32_t` without including ``. + +### Fix + +Add `#include ` **before** SDK headers: + +```cpp +#include +#include // Add this! +#include "zoom_video_sdk_api.h" +``` + +--- + +## Error: 'YUVRawDataI420' undeclared identifier + +### Symptom + +``` +error C2065: 'YUVRawDataI420': undeclared identifier +``` + +### Cause + +The class is only forward-declared in some headers. + +### Fix + +Include the raw data definition header: + +```cpp +#include "zoom_sdk_raw_data_def.h" +``` + +--- + +## Error: Cannot open include file 'json/json.h' + +### Symptom + +``` +fatal error C1083: Cannot open include file: 'json/json.h': No such file or directory +``` + +### Cause + +jsoncpp library not installed or not in include path. + +### Fix + +Install via vcpkg and configure include paths: + +```powershell +# Install vcpkg +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install + +# Install jsoncpp +.\vcpkg install jsoncpp:x64-windows +``` + +Then add to **Additional Include Directories**: +``` +C:\vcpkg\packages\jsoncpp_x64-windows\include +``` + +--- + +## Error: unresolved external symbol 'CreateZoomVideoSDKObj' + +### Symptom + +``` +error LNK2019: unresolved external symbol "CreateZoomVideoSDKObj" +``` + +### Cause + +SDK library not linked. + +### Fix + +1. Add to **Additional Library Directories**: + ``` + $(SolutionDir)SDK\lib + ``` + +2. Add to **Additional Dependencies**: + ``` + sdk.lib + ``` + +--- + +## Error: sdk.dll not found + +### Symptom + +``` +The code execution cannot proceed because sdk.dll was not found. +``` + +### Cause + +SDK DLLs not in output directory. + +### Fix + +Add Post-Build Event: + +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +``` + +Or manually copy all DLLs from `SDK\bin\` to your output directory. + +--- + +## Error: Abstract class cannot be instantiated + +### Symptom + +``` +error C2259: 'MyDelegate': cannot instantiate abstract class +``` + +### Cause + +Not all pure virtual methods implemented in your delegate class. + +### Fix + +Implement ALL 80+ methods in `IZoomVideoSDKDelegate`. Even unused methods need empty implementations: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // Implement all methods, even if empty + void onSessionJoin() override { /* your code */ } + void onSessionLeave() override {} + void onError(ZoomVideoSDKErrors, int) override {} + void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + // ... all 80+ methods +}; +``` + +See [Delegate Methods](../references/delegate-methods.md) for complete list. + +--- + +## Error: 'IZoomVideoSDK' is undefined + +### Symptom + +``` +error C2027: use of undefined type 'ZOOMVIDEOSDK_NAMESPACE::IZoomVideoSDK' +``` + +### Cause + +Missing include or namespace. + +### Fix + +```cpp +#include "zoom_video_sdk_interface.h" + +// Use namespace +using namespace ZOOMVIDEOSDK_NAMESPACE; +// Or +USING_ZOOM_VIDEO_SDK_NAMESPACE +``` + +--- + +## Visual Studio Project Configuration + +### Include Directories + +**C/C++ → General → Additional Include Directories:** + +``` +$(SolutionDir)SDK\h +$(SolutionDir)SDK\h\helpers +C:\vcpkg\packages\jsoncpp_x64-windows\include +``` + +### Library Directories + +**Linker → General → Additional Library Directories:** + +``` +$(SolutionDir)SDK\lib +C:\vcpkg\packages\jsoncpp_x64-windows\lib +``` + +### Additional Dependencies + +**Linker → Input → Additional Dependencies:** + +``` +sdk.lib +jsoncpp.lib +``` + +### Post-Build Event + +**Build Events → Post-Build Event → Command Line:** + +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +xcopy /Y /D "$(ProjectDir)config.json" "$(OutDir)" +``` + +### Runtime Library + +**C/C++ → Code Generation → Runtime Library:** + +- Debug: Multi-threaded Debug DLL (/MDd) +- Release: Multi-threaded DLL (/MD) + +--- + +## MSBuild from Git Bash + +When building from Git Bash, use this pattern: + +```bash +MSYS_NO_PATHCONV=1 "/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe" \ + YourProject.vcxproj \ + /p:Configuration=Release \ + /p:Platform=x64 \ + /t:Build \ + /verbosity:minimal +``` + +**Key points:** +- `MSYS_NO_PATHCONV=1` prevents path conversion issues +- Use full path to MSBuild.exe in quotes +- Escape or quote paths with spaces + +--- + +## SDK Directory Structure + +Expected structure: + +``` +YourProject/ +├── SDK/ +│ ├── bin/ # DLLs (copy to output) +│ │ ├── sdk.dll +│ │ ├── *.dll # Many other DLLs +│ ├── h/ # Headers +│ │ ├── zoom_video_sdk_api.h +│ │ ├── zoom_video_sdk_interface.h +│ │ ├── zoom_video_sdk_delegate_interface.h +│ │ ├── zoom_sdk_raw_data_def.h +│ │ └── helpers/ +│ │ └── *.h +│ └── lib/ # Libraries +│ └── sdk.lib +├── YourProject.cpp +├── YourProject.vcxproj +└── config.json +``` + +--- + +## Related Documentation + +- [Windows Reference](../references/windows-reference.md) - Full project setup +- [Delegate Methods](../references/delegate-methods.md) - All required callbacks +- [Common Issues](common-issues.md) - Runtime troubleshooting + +--- + +**TL;DR**: Include `` first, then ``, then SDK headers. Link `sdk.lib`. Copy DLLs to output. Implement all 80+ delegate methods. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/common-issues.md new file mode 100644 index 00000000..7ec336fa --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/common-issues.md @@ -0,0 +1,289 @@ +# Common Issues + +Quick diagnostic guide for Zoom Video SDK Windows issues. + +--- + +## Quick Diagnostic Checklist + +| Symptom | Likely Cause | Solution | +|---------|--------------|----------| +| Callbacks don't fire | Missing message loop | [Windows Message Loop](windows-message-loop.md) | +| Build errors | Include order / missing headers | [Build Errors](build-errors.md) | +| Abstract class error | Missing delegate methods | [Delegate Methods](../references/delegate-methods.md) | +| Video subscribe fails | Subscribing too early | Subscribe in `onUserVideoStatusChanged` | +| Error code 2 | Video not ready | Wait for `status.isOn == true` | +| Error code 8 | Too frequent calls | Add `Sleep(200)` between calls | +| DLL not found | DLLs not copied | Copy SDK `bin\` to output | + +--- + +## Error Codes (ZoomVideoSDKErrors) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| 0 | Success | Operation succeeded | - | +| 1 | Wrong_Usage | API called incorrectly | Check API documentation | +| 2 | Internal_Error | SDK internal error | Often: video not ready yet | +| 3 | Uninitialize | SDK not initialized | Call `initialize()` first | +| 4 | Memory_Error | Memory allocation failed | Check system resources | +| 5 | Load_Module_Error | Failed to load DLL | Check DLLs are present | +| 6 | UnLoad_Module_Error | Failed to unload DLL | - | +| 7 | Invalid_Parameter | Bad parameter | Check NULL pointers, HWNDs | +| 8 | Call_Too_Frequently | API called too often | Add `Sleep(200)` | +| 9 | No_Impl | Feature not implemented | - | +| 10 | Dont_Support_Feature | Feature not supported | Check SDK version | +| 100 | Unknown | Unknown error | Check logs | + +### Auth Errors (1000+) + +| Code | Name | Solution | +|------|------|----------| +| 1001 | Auth_Error | Check JWT token | +| 1002 | Auth_Empty_Key_or_Secret | Provide credentials | +| 1003 | Auth_Wrong_Key_or_Secret | Verify credentials | +| 1004 | Auth_DoesNot_Support_SDK | Check SDK version | +| 1005 | Auth_Disable_SDK | Contact Zoom support | + +### Session Errors (3000+) + +| Code | Name | Solution | +|------|------|----------| +| 3001 | Session_Join_Failed | Check session name/token | +| 3002 | Session_No_Rights | Check permissions | +| 3003 | Session_Already_In_Progress | Leave first | +| 3005 | Session_Reconnecting | Wait for reconnect | +| 3008 | Session_Need_Password | Provide password | +| 3009 | Session_Password_Wrong | Check password | + +--- + +## Subscribe Fail Reasons + +When `onVideoCanvasSubscribeFail` fires: + +| Code | Reason | Solution | +|------|--------|----------| +| 0 | None | - | +| 1 | HasSubscribe1080POr720P | Already have HD subscription | +| 2 | HasSubscribeTwo720P | Max 2x 720p | +| 3 | HasSubscribeExceededLimit | Too many subscriptions | +| 4 | HasSubscribeTwoShare | Max 2 share subscriptions | +| 5 | HasSubscribeVideo1080POr720PAndOneShare | Combined limit | +| 6 | TooFrequentCall | Add `Sleep(200)` | + +--- + +## Common Issues by Category + +### Session Issues + +#### "joinSession returns success but onSessionJoin never fires" + +**Cause**: Missing Windows message loop + +**Fix**: +```cpp +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +#### "Session join fails with error 3001" + +**Cause**: Invalid JWT token or session name + +**Fix**: +1. Verify JWT token is valid and not expired +2. Check session name matches token +3. Ensure token has correct role (host/attendee) + +--- + +### Video Issues + +#### "subscribeWithView returns error 2" + +**Cause**: Video not ready when subscribing + +**Fix**: Subscribe in `onUserVideoStatusChanged`, not `onUserJoin`: + +```cpp +void onUserVideoStatusChanged(..., userList) override { + for (auto user : userList) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); + } + } +} +``` + +#### "Video shows black screen" + +**Causes**: +1. Camera not started +2. Wrong HWND +3. Subscription failed silently + +**Fixes**: +```cpp +// 1. Start your camera +sdk->getVideoHelper()->startVideo(); + +// 2. Verify HWND is valid and visible +HWND hwnd = CreateWindow(...); +ShowWindow(hwnd, SW_SHOW); + +// 3. Check subscribe return value +ZoomVideoSDKErrors err = canvas->subscribeWithView(hwnd, ...); +if (err != ZoomVideoSDKErrors_Success) { + std::cout << "Subscribe failed: " << err << std::endl; +} +``` + +#### "onVideoCanvasSubscribeFail fires with reason 6" + +**Cause**: Calling subscribe too frequently + +**Fix**: Add delay between subscribe calls: +```cpp +canvas->subscribeWithView(hwnd1, ...); +Sleep(200); // Add delay +canvas->subscribeWithView(hwnd2, ...); +``` + +--- + +### Audio Issues + +#### "No audio after joining" + +**Cause**: Audio not connected + +**Fix**: Connect audio in `onSessionJoin`: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); +} +``` + +Also ensure `audioOption.connect = false` during join: +```cpp +context.audioOption.connect = false; // Connect in callback +``` + +#### "Cannot mute/unmute" + +**Cause**: Trying to control audio before connected + +**Fix**: Wait for audio to be connected: +```cpp +void onUserAudioStatusChanged(...) override { + if (user->getAudioStatus().isAudioConnected) { + // Now safe to mute/unmute + } +} +``` + +--- + +### Build Issues + +#### "Cannot instantiate abstract class" + +**Cause**: Not all delegate methods implemented + +**Fix**: Implement ALL 80+ methods: +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { + void onSessionJoin() override { } + void onSessionLeave() override { } + void onError(ZoomVideoSDKErrors, int) override { } + // ... all 80+ methods +}; +``` + +See [Delegate Methods](../references/delegate-methods.md). + +#### "Unresolved external symbol" + +**Cause**: sdk.lib not linked + +**Fix**: Add to linker settings: +- Additional Library Directories: `$(SolutionDir)SDK\lib` +- Additional Dependencies: `sdk.lib` + +--- + +### Runtime Issues + +#### "DLL not found" + +**Cause**: SDK DLLs not in executable directory + +**Fix**: Copy all DLLs from `SDK\bin\` to output directory. + +Post-Build Event: +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +``` + +#### "Application crashes on exit" + +**Cause**: Cleanup order incorrect + +**Fix**: Proper cleanup sequence: +```cpp +void Cleanup() { + // 1. Leave session first + if (sdk->isInSession()) { + sdk->leaveSession(false); + } + + // 2. Wait for onSessionLeave callback + // (process messages while waiting) + + // 3. Cleanup SDK + sdk->cleanup(); + + // 4. Destroy SDK object + DestroyZoomVideoSDKObj(); +} +``` + +--- + +## Logging + +Enable SDK logging for debugging: + +```cpp +ZoomVideoSDKInitParams params; +params.enableLog = true; +params.logFilePrefix = L"zoom_video_sdk"; +``` + +Logs are written to: `%APPDATA%\ZoomVideoSDK\logs\` + +--- + +## Related Documentation + +- [Windows Message Loop](windows-message-loop.md) - Callback issues +- [Build Errors](build-errors.md) - Compile/link errors +- [Delegate Methods](../references/delegate-methods.md) - Required callbacks +- [Video Rendering](../examples/video-rendering.md) - Video subscription +- [API Reference](../references/windows-reference.md) - Error codes + +--- + +**Quick fixes:** +1. Callbacks don't fire → Add message loop +2. Error 2 → Subscribe in `onUserVideoStatusChanged` +3. Error 8 → Add `Sleep(200)` between calls +4. Abstract class → Implement all 80+ delegate methods diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/windows-message-loop.md b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/windows-message-loop.md new file mode 100644 index 00000000..7110b0ca --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/troubleshooting/windows-message-loop.md @@ -0,0 +1,254 @@ +# Windows Message Loop + +## The #1 Cause of "Callbacks Don't Fire" + +If your SDK callbacks aren't firing (onSessionJoin, onUserJoin, etc.), you're almost certainly missing the Windows message loop. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SYMPTOM │ CAUSE │ +├─────────────────────────────────────────────────────────────────┤ +│ joinSession() returns success │ │ +│ but onSessionJoin() never │ Missing Windows message loop │ +│ fires │ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Why It's Required + +The Zoom Video SDK uses **Windows messaging** to dispatch callbacks. When an event occurs (user joins, video starts, etc.), the SDK posts a message to your thread's message queue. If you never process those messages, the callbacks never fire. + +``` +SDK Event Occurs + ↓ +SDK posts message to your thread's queue + ↓ +Your message loop calls PeekMessage/GetMessage + ↓ +Message is dispatched + ↓ +Your callback fires +``` + +**Without the message loop, step 3 never happens.** + +--- + +## The Fix + +### Console Application (No GUI) + +```cpp +int main() { + // Initialize SDK + IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + sdk->initialize(params); + sdk->addListener(new MyDelegate()); + sdk->joinSession(context); + + // ═══════════════════════════════════════════════════════════════ + // CRITICAL: Windows message loop + // ═══════════════════════════════════════════════════════════════ + bool running = true; + while (running) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + running = false; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid 100% CPU + Sleep(10); + } + + // Cleanup + sdk->leaveSession(false); + sdk->cleanup(); + + return 0; +} +``` + +### GUI Application (WinMain) + +GUI applications using standard WinMain already have a message loop, but make sure it's running: + +```cpp +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPSTR lpCmdLine, int nCmdShow) { + // Create window, initialize SDK, etc. + + // Standard message loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return (int)msg.wParam; +} +``` + +--- + +## Common Mistakes + +### Mistake 1: No Message Loop At All + +```cpp +// WRONG - callbacks will never fire +int main() { + sdk->joinSession(context); + + // Waiting forever, but callbacks never fire + while (!g_joined) { + Sleep(100); // No message processing! + } +} +``` + +### Mistake 2: Message Loop in Wrong Thread + +```cpp +// WRONG - SDK callbacks are tied to the thread that called joinSession +void WorkerThread() { + sdk->joinSession(context); // Callbacks tied to this thread +} + +int main() { + std::thread worker(WorkerThread); + + // Message loop on main thread won't help worker thread's callbacks + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} +``` + +**Fix**: Run message loop on the same thread that calls SDK methods. + +### Mistake 3: Blocking the Message Loop + +```cpp +// WRONG - blocking call prevents message processing +void onSessionJoin() override { + // This blocks the message loop! + std::this_thread::sleep_for(std::chrono::seconds(10)); + + // Or this: + while (waiting) { } // Infinite loop blocks everything +} +``` + +**Fix**: Keep callbacks fast. Use async/threading for long operations. + +--- + +## Diagnostic Checklist + +If callbacks aren't firing: + +1. **Is there a message loop?** + - Look for `PeekMessage` or `GetMessage` in your code + - Must be on the same thread that calls `joinSession()` + +2. **Is the message loop running?** + - Add logging: `std::cout << "Processing messages..." << std::endl;` + - Should print continuously + +3. **Is the delegate registered?** + - `sdk->addListener(delegate)` must be called BEFORE `joinSession()` + +4. **Are all delegate methods implemented?** + - Missing pure virtual methods = compile error + - Wrong signature = callback not called + +5. **Is the SDK initialized?** + - Check return value of `initialize()` + +--- + +## Minimal Working Example + +```cpp +#include +#include +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +bool g_joined = false; + +class TestDelegate : public IZoomVideoSDKDelegate { +public: + void onSessionJoin() override { + std::cout << "*** onSessionJoin fired! ***" << std::endl; + g_joined = true; + } + + void onError(ZoomVideoSDKErrors err, int detail) override { + std::cout << "*** onError: " << err << " ***" << std::endl; + } + + // ... implement all other methods as empty +}; + +int main() { + IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + + ZoomVideoSDKInitParams params; + params.domain = L"https://zoom.us"; + sdk->initialize(params); + + sdk->addListener(new TestDelegate()); + + ZoomVideoSDKSessionContext ctx; + ctx.sessionName = L"test"; + ctx.userName = L"Bot"; + ctx.token = L"your-jwt"; + ctx.audioOption.connect = false; + + sdk->joinSession(ctx); + + std::cout << "Starting message loop..." << std::endl; + + // THE CRITICAL PART + while (!g_joined) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); + } + + std::cout << "Joined! Exiting..." << std::endl; + + sdk->leaveSession(false); + sdk->cleanup(); + + return 0; +} +``` + +--- + +## Related Documentation + +- [Session Join Pattern](../examples/session-join-pattern.md) - Complete working code +- [Common Issues](common-issues.md) - Other troubleshooting +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Event-driven design + +--- + +**TL;DR**: Add `PeekMessage`/`TranslateMessage`/`DispatchMessage` loop on the same thread that calls `joinSession()`. This is NOT optional. diff --git a/partner-built/zoom-plugin/skills/video-sdk/windows/windows.md b/partner-built/zoom-plugin/skills/video-sdk/windows/windows.md new file mode 100644 index 00000000..39625371 --- /dev/null +++ b/partner-built/zoom-plugin/skills/video-sdk/windows/windows.md @@ -0,0 +1,46 @@ +--- +name: video-sdk/windows +description: "Zoom Video SDK for Windows - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication" +--- + +# Zoom Video SDK (Windows) + +Windows platform support for Zoom Video SDK. Build custom video applications with C++ or C#/.NET integration. + +For complete documentation, see **[SKILL.md](SKILL.md)** + +## Quick Links + +- **[Windows SDK Guide](SKILL.md)** - Complete Windows development guide +- **[API Reference](references/windows-reference.md)** - Complete API documentation +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ + +## Features + +- Full video session control +- Raw audio/video capture (PCM, YUV I420) +- Raw media injection (virtual audio/video) +- Cloud recording & live streaming +- Multi-platform support (x64, x86, ARM64) + +## UI Framework Integration + +| Framework | Approach | Guide | +|-----------|----------|-------| +| **Win32** | Direct SDK + Canvas API | [Win32 Guide](examples/dotnet-winforms/README.md#option-1-win32-native-c---direct-sdk) | +| **WinForms** | C++/CLI wrapper + Raw Data | [WinForms Guide](examples/dotnet-winforms/README.md#option-2-winforms-c--ccli-wrapper) | +| **WPF** | C++/CLI wrapper + BitmapSource | [WPF Guide](examples/dotnet-winforms/README.md#option-3-wpf-c--ccli-wrapper) | + +## C++/CLI Wrapper Patterns (Any Native Library → .NET) + +Complete 8-pattern guide for wrapping native C++ libraries: +- [Full Guide](examples/dotnet-winforms/README.md#ccli-wrapper-patterns-for-net-integration) +- Patterns: void*, gcroot, Finalizer, Strings, Arrays, Threading, LockBits + +## Sample Repositories + +- **GitHub**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Local Samples**: + - `C:\tempsdk\Zoom_VideoSDK_Windows_RawDataDemos\` - C++ demos + - `C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\` - C# demos + - `C:\tempsdk\sdksamples\zoom-video-sdk-windows-2.4.12\` - Official SDK samples diff --git a/partner-built/zoom-plugin/skills/virtual-agent/RUNBOOK.md b/partner-built/zoom-plugin/skills/virtual-agent/RUNBOOK.md new file mode 100644 index 00000000..434b8576 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/RUNBOOK.md @@ -0,0 +1,31 @@ +# Virtual Agent 5-Minute Runbook + +## 1. Credentials and Product Access + +- Confirm Virtual Agent license is active. +- Confirm campaign or entry ID exists and is published. +- Confirm API key and environment (`us01` or `eu01`) are correct. + +## 2. Browser or WebView Readiness + +- Verify CSP allows Zoom SDK script, websocket, media, and wasm execution. +- Verify no blocker/proxy is stripping `zcc-sdk.js`. +- For WebView, verify JavaScript is enabled. + +## 3. Lifecycle Order + +- Load SDK script. +- Wait for `zoomCampaignSdk:ready` or `waitForReady()`. +- Register event handlers. +- Call `open()` / `show()` only after readiness. + +## 4. Native Bridge (Android/iOS) + +- Inject `window.zoomCampaignSdk.native` on readiness. +- Wire `exitHandler`, `commonHandler`, and `support_handoff` callbacks. +- Verify URL policy (`target="_blank"`, `window.open`) is implemented. + +## 5. Drift Check + +- Validate docs naming (`Virtual Agent`) vs sample naming (`Virtual Assistant` / `LiveSDK`). +- Treat `openURL` command path as legacy/deprecated and prefer DOM links or `window.open`. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/SKILL.md b/partner-built/zoom-plugin/skills/virtual-agent/SKILL.md new file mode 100644 index 00000000..19e8606a --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/SKILL.md @@ -0,0 +1,73 @@ +--- +name: build-zoom-virtual-agent +description: "Reference skill for Zoom Virtual Agent. Use after routing to a virtual-agent workflow when implementing web embeds, Android or iOS wrapper integrations, knowledge-base sync, lifecycle handling, or troubleshooting." +triggers: + - "virtual agent" + - "zva" + - "virtual assistant sdk" + - "knowledge base sync" +--- + +# /build-zoom-virtual-agent + +Background reference for Zoom Virtual Agent across: +- Web campaign/chat embeds. +- Android WebView wrappers. +- iOS WKWebView wrappers. +- Knowledge-base sync and custom API ingestion. + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/ +- https://developers.zoom.us/docs/virtual-agent/web/ +- https://developers.zoom.us/docs/virtual-agent/android/ +- https://developers.zoom.us/docs/virtual-agent/ios/ + +## Routing Guardrail + +- If the user is implementing Contact Center app surfaces inside Zoom client, chain with [../contact-center/SKILL.md](../contact-center/SKILL.md). +- If the user needs backend knowledge-base CRUD or automation scripts, chain with [../rest-api/SKILL.md](../rest-api/SKILL.md) and [../oauth/SKILL.md](../oauth/SKILL.md). +- If the user asks only for website bot embed and campaign controls, stay on [web/SKILL.md](web/SKILL.md). +- If the user asks for mobile native wrappers around web chat, route to [android/SKILL.md](android/SKILL.md) or [ios/SKILL.md](ios/SKILL.md). + +## Quick Links + +1. [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +3. [references/versioning-and-drift.md](references/versioning-and-drift.md) +4. [references/samples-validation.md](references/samples-validation.md) +5. [references/environment-variables.md](references/environment-variables.md) +6. [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) +7. [RUNBOOK.md](RUNBOOK.md) + +Platform skills: +- [web/SKILL.md](web/SKILL.md) +- [android/SKILL.md](android/SKILL.md) +- [ios/SKILL.md](ios/SKILL.md) + +## Common Lifecycle Pattern + +1. Configure campaign or entry ID in Virtual Agent admin. +2. Initialize SDK in web or WebView container. +3. Wait for readiness (`zoomCampaignSdk:ready` or `waitForReady()`) before calling APIs. +4. Register bridge handlers (`exitHandler`, `commonHandler`, `support_handoff`) when native orchestration is needed. +5. Handle conversation lifecycle (`engagement_started`, `engagement_ended`) and UI state. +6. End chat (`endChat`) and clean up listeners. + +## High-Level Scenarios + +- Website campaign launcher with contextual customer attributes. +- Mobile app WebView chat with native close/handoff bridge. +- External URL handling via system browser vs in-app browser policy. +- Knowledge-base sync from external systems using custom API connector. +- Cross-team support flow that escalates from bot to live support with handoff payload. + +## Chaining + +- Contact Center app/web/mobile patterns: [../contact-center/SKILL.md](../contact-center/SKILL.md) +- OAuth app setup and tokens: [../oauth/SKILL.md](../oauth/SKILL.md) +- API workflows for KB automation: [../rest-api/SKILL.md](../rest-api/SKILL.md) +- Event-driven backend follow-up: [../webhooks/SKILL.md](../webhooks/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/android/SKILL.md b/partner-built/zoom-plugin/skills/virtual-agent/android/SKILL.md new file mode 100644 index 00000000..373aa7b8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/android/SKILL.md @@ -0,0 +1,41 @@ +--- +name: virtual-agent/android +description: "Zoom Virtual Agent Android integration via WebView. Use for Java/Kotlin bridge callbacks, native URL handling, support_handoff relay, and lifecycle-safe embedding." +user-invocable: false +triggers: + - "virtual agent android" + - "android webview zva" + - "zoomCampaignSdk:ready android" + - "support_handoff android" + - "javascriptinterface" +--- + +# Zoom Virtual Agent - Android + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/android/ + +## Quick Links + +1. [concepts/webview-lifecycle.md](concepts/webview-lifecycle.md) +2. [examples/js-bridge-patterns.md](examples/js-bridge-patterns.md) +3. [references/android-reference-map.md](references/android-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Model + +- Host campaign URL in Android WebView. +- Inject runtime context (`window.zoomCampaignSdkConfig`). +- Register JavaScript bridge for `exitHandler`, `commonHandler`, `support_handoff`. +- Apply URL policy via `shouldOverrideUrlLoading` and optional multi-window callbacks. + +## Hard Guardrails + +- Initialize handlers before expecting JS callbacks. +- Treat legacy `openURL` command handling as compatibility path only. +- Prefer DOM links or `window.open` handling plus explicit native routing. + +## Chaining + +- Product-level patterns: [../SKILL.md](../SKILL.md) +- Contact Center mobile scope: [../../contact-center/android/SKILL.md](../../contact-center/android/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/virtual-agent/android/concepts/webview-lifecycle.md b/partner-built/zoom-plugin/skills/virtual-agent/android/concepts/webview-lifecycle.md new file mode 100644 index 00000000..a74677d8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/android/concepts/webview-lifecycle.md @@ -0,0 +1,9 @@ +# Android WebView Lifecycle + +1. Build intent with URL and policy flags. +2. Configure WebView (`JavaScriptEnabled`, optional multi-window support). +3. Inject user context config before page interaction. +4. Inject bridge script on `zoomCampaignSdk:ready`. +5. Handle callbacks via `@JavascriptInterface`. +6. Route URL actions and handoff payloads. +7. Close view and clean references on exit. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/android/examples/js-bridge-patterns.md b/partner-built/zoom-plugin/skills/virtual-agent/android/examples/js-bridge-patterns.md new file mode 100644 index 00000000..b449b33e --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/android/examples/js-bridge-patterns.md @@ -0,0 +1,37 @@ +# Android JS Bridge Patterns + +## Inject Native Bridge + +```kotlin +private fun injectJavaScriptFunction() { + val js = """ + javascript: window.addEventListener('zoomCampaignSdk:ready', () => { + if (window.zoomCampaignSdk) { + window.zoomCampaignSdk.native = { + exitHandler: { handle: function() { AndroidExit.handleExit(); } }, + commonHandler: { handle: function(e) { AndroidCommon.handleCommon(JSON.stringify(e)); } } + }; + } + }); + """.trimIndent() + webView.loadUrl(js) +} +``` + +## Handoff Relay + +```kotlin +private fun injectHandoffFunction() { + val js = """ + javascript: window.addEventListener('support_handoff', (e) => { + AndroidHandoff.handleHandoff(JSON.stringify(e.detail)); + }); + """.trimIndent() + webView.loadUrl(js) +} +``` + +## URL Governance + +- Use `shouldOverrideUrlLoading` for in-app vs system-browser policy. +- Use multi-window callbacks for `target="_blank"` handling. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/android/references/android-reference-map.md b/partner-built/zoom-plugin/skills/virtual-agent/android/references/android-reference-map.md new file mode 100644 index 00000000..e0b5ae7d --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/android/references/android-reference-map.md @@ -0,0 +1,14 @@ +# Android Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/android/get-started/ +- Integration scenarios: https://developers.zoom.us/docs/virtual-agent/android/integration-scenarios/ +- JavaScript events: https://developers.zoom.us/docs/virtual-agent/android/javascript-events/ +- Resources: https://developers.zoom.us/docs/virtual-agent/android/resources/ + +## Observed Sample Patterns + +- Java and Kotlin implementations follow the same bridge contract. +- Bridge command routing centers around `commonHandler` JSON payloads. +- `support_handoff` events are emitted from JS and consumed in native layer. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/android/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/virtual-agent/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..8e0096a3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/android/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Android Common Issues + +## Bridge Callback Never Fires + +- Ensure JS bridge injection runs after page load and SDK readiness. +- Ensure `addJavascriptInterface` names match injected handler names. + +## Link Opens in Wrong Context + +- Implement both `shouldOverrideUrlLoading` and multi-window behavior. +- Distinguish `_self` and `_blank` paths explicitly. + +## Deprecated `openURL` Path + +- Avoid relying on `{"cmd":"openURL"...}` as primary flow. +- Prefer anchor or `window.open` plus native interception policy. + +## Campaign Works on Web but Not Mobile + +- Verify campaign targeting includes mobile. +- Verify same API key/env pair used in WebView build. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/concepts/architecture-and-lifecycle.md b/partner-built/zoom-plugin/skills/virtual-agent/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..79021072 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/concepts/architecture-and-lifecycle.md @@ -0,0 +1,27 @@ +# Architecture and Lifecycle + +## Architecture + +```text +Web or Mobile Host App + -> Zoom Campaign SDK (zcc-sdk.js) + -> Campaign or Entry routing + -> Bot conversation state + -> Optional native bridge (Android/iOS) + -> Optional backend automation (OAuth + REST KB APIs) +``` + +## Lifecycle + +1. Provision bot flow and campaign/entry configuration. +2. Embed SDK snippet and provide runtime config (`apikey`, `env`, user context). +3. Wait for readiness, then register event callbacks. +4. Start or show engagement (`open`, `show`). +5. React to events (`open`, `close`, `engagement_started`, `engagement_ended`). +6. For mobile wrappers, forward bridge events (`support_handoff`, exit, URL commands). +7. End session (`endChat`) and detach handlers. + +## Version Drift Notes + +- Documentation now uses "Virtual Agent" naming. +- Official sample repositories still contain older naming (`virtual-assistant`, `liveSDK`) that can mislead search and code mapping. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/ios/SKILL.md b/partner-built/zoom-plugin/skills/virtual-agent/ios/SKILL.md new file mode 100644 index 00000000..5d72ed9b --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/ios/SKILL.md @@ -0,0 +1,41 @@ +--- +name: virtual-agent/ios +description: "Zoom Virtual Agent iOS integration via WKWebView. Use for Swift/Objective-C script injection, message handlers, support_handoff relay, and URL routing policies." +user-invocable: false +triggers: + - "virtual agent ios" + - "wkwebview zva" + - "support_handoff ios" + - "zoomCampaignSdk:ready ios" + - "wkusercontentcontroller" +--- + +# Zoom Virtual Agent - iOS + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/ios/ + +## Quick Links + +1. [concepts/webview-lifecycle.md](concepts/webview-lifecycle.md) +2. [examples/js-bridge-patterns.md](examples/js-bridge-patterns.md) +3. [references/ios-reference-map.md](references/ios-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Model + +- Load campaign URL in `WKWebView`. +- Inject `window.zoomCampaignSdkConfig` using `WKUserScript`. +- Register message handlers for exit/common/handoff flows. +- Handle URL behavior in navigation delegates (`in-app`, `SFSafariViewController`, or system browser). + +## Hard Guardrails + +- Register scripts and handlers before web interaction. +- Handle iOS 14.5+ download behavior where needed. +- Keep deprecated `openURL` command support as fallback only. + +## Chaining + +- Product-level patterns: [../SKILL.md](../SKILL.md) +- Contact Center mobile scope: [../../contact-center/ios/SKILL.md](../../contact-center/ios/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/virtual-agent/ios/concepts/webview-lifecycle.md b/partner-built/zoom-plugin/skills/virtual-agent/ios/concepts/webview-lifecycle.md new file mode 100644 index 00000000..ebb23c3b --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/ios/concepts/webview-lifecycle.md @@ -0,0 +1,9 @@ +# iOS WKWebView Lifecycle + +1. Create `WKWebViewConfiguration` and `WKUserContentController`. +2. Add user scripts for context injection and bridge handlers. +3. Register message handlers before navigation. +4. Push/present webview controller with campaign URL. +5. Process callbacks in `userContentController:didReceiveScriptMessage:`. +6. Route navigation and external links in WKNavigation delegate callbacks. +7. Remove message handlers during teardown. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/ios/examples/js-bridge-patterns.md b/partner-built/zoom-plugin/skills/virtual-agent/ios/examples/js-bridge-patterns.md new file mode 100644 index 00000000..f214e264 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/ios/examples/js-bridge-patterns.md @@ -0,0 +1,32 @@ +# iOS JS Bridge Patterns + +## Inject Exit and Common Handlers + +```swift +let exitHandlerScript = """ +window.addEventListener('zoomCampaignSdk:ready', () => { + if (window.zoomCampaignSdk) { + window.zoomCampaignSdk.native = { + exitHandler: { handle: function() { window.webkit.messageHandlers.zoomLiveSDKMessageHandler.postMessage('close_web_vc'); } }, + commonHandler: { handle: function(e) { window.webkit.messageHandlers.commonMessageHandler.postMessage(JSON.stringify(e)); } } + }; + } +}); +""" +``` + +## Inject Support Handoff + +```swift +let handoffScript = """ +window.addEventListener('support_handoff', (e) => { + window.webkit.messageHandlers.support_handoff.postMessage(JSON.stringify(e.detail)); +}); +""" +``` + +## URL Handling Policy + +- `WKNavigationActionPolicyAllow` for trusted in-app routes. +- `UIApplication.openURL` for system-browser policy. +- Optional in-app browser route (for example `SFSafariViewController`). diff --git a/partner-built/zoom-plugin/skills/virtual-agent/ios/references/ios-reference-map.md b/partner-built/zoom-plugin/skills/virtual-agent/ios/references/ios-reference-map.md new file mode 100644 index 00000000..f94a4c83 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/ios/references/ios-reference-map.md @@ -0,0 +1,14 @@ +# iOS Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/ios/get-started/ +- Integration scenarios: https://developers.zoom.us/docs/virtual-agent/ios/integration-scenarios/ +- JavaScript events: https://developers.zoom.us/docs/virtual-agent/ios/javascript-events/ +- Resources: https://developers.zoom.us/docs/virtual-agent/ios/resources/ + +## Observed Sample Patterns + +- Objective-C and Swift variants expose equivalent bridge behavior. +- Message handler constants in sample code still use legacy naming. +- URL routing policy is split between delegate interception and message-handler command processing. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/ios/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/virtual-agent/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..fc9f2d92 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/ios/troubleshooting/common-issues.md @@ -0,0 +1,20 @@ +# iOS Common Issues + +## Message Handlers Not Triggering + +- Ensure `WKUserScript` and handlers are registered before page load. +- Verify handler names exactly match injected JS references. + +## URL Opens Unexpectedly + +- Explicitly branch in `decidePolicyForNavigationAction`. +- Handle `_blank`/`window.open` paths as separate cases. + +## Deprecated `openURL` Command Drift + +- Treat command-based open URL as fallback. +- Prefer DOM links and `window.open` with delegate-driven routing. + +## File Download Inconsistency + +- Download behavior via WKWebView requires iOS 14.5+ support paths. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/references/environment-variables.md b/partner-built/zoom-plugin/skills/virtual-agent/references/environment-variables.md new file mode 100644 index 00000000..1efef71f --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/references/environment-variables.md @@ -0,0 +1,22 @@ +# Environment Variables + +## Web Embed Runtime + +- `ZVA_API_KEY`: Virtual Agent API key for campaign SDK script. +- `ZVA_ENV`: Deployment region (`us01` or `eu01`). +- `ZVA_CAMPAIGN_ID`: Optional campaign identifier when switching campaigns programmatically. +- `ZVA_ENTRY_ID`: Optional entry ID path when campaign mode is not used. + +## Knowledge Base API Automation + +- `ZOOM_ACCOUNT_ID`: Account ID for Server-to-Server OAuth app. +- `ZOOM_CLIENT_ID`: S2S OAuth client ID. +- `ZOOM_CLIENT_SECRET`: S2S OAuth client secret. +- `ZOOM_ACCESS_TOKEN`: Runtime bearer token (short-lived). +- `ZVA_KB_ID`: Knowledge base ID for custom API sync. + +## Where to Find Keys + +1. Zoom Marketplace app credentials: OAuth app in Marketplace. +2. Virtual Agent campaign/entry settings: Zoom admin portal AI Management. +3. KB ID: Knowledge Base settings in AI Management. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/references/samples-validation.md b/partner-built/zoom-plugin/skills/virtual-agent/references/samples-validation.md new file mode 100644 index 00000000..e1899838 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/references/samples-validation.md @@ -0,0 +1,22 @@ +# Samples Validation + +Validated repositories: +- https://github.com/zoom/virtual-assistant-android-sample +- https://github.com/zoom/virtual-assistant-iOS-sample + +Latest commit observed during validation: +- Android sample: `faab2b6` (2024-10-16), commit message references `OpenUrl` deprecation. +- iOS sample: `dd31e95` (2024-10-16), commit message references URL opening method update. + +## Confirmed Relevant Patterns + +- `zoomCampaignSdk:ready` event gating before native bridge registration. +- `window.zoomCampaignSdk.native.exitHandler/commonHandler` bridge contract. +- `support_handoff` event forwarding from JavaScript to native. +- WebView URL policy split between in-app browsing and system browser. + +## Contradictions and Caveats + +- Samples still document legacy command contract (`{"cmd":"openURL","value":"..."}`) while marking it deprecated. +- Naming in sample classes uses "LiveSDK" and "Virtual Assistant" while docs use "Virtual Agent". +- Treat sample repos as implementation patterns, not canonical naming source. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/references/versioning-and-drift.md b/partner-built/zoom-plugin/skills/virtual-agent/references/versioning-and-drift.md new file mode 100644 index 00000000..a70de270 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/references/versioning-and-drift.md @@ -0,0 +1,21 @@ +# Versioning and Drift + +## Naming Drift + +- Current docs and product naming: **Virtual Agent**. +- Sample repo naming still includes legacy terms: **virtual-assistant**, **liveSDK**, `ZMLiveSDKWebviewController`. +- Integration code should follow current docs semantics while mapping legacy symbol names from samples. + +## Deprecated or Legacy Patterns + +- `openURL` command JSON payload is marked deprecated in 2024 sample code comments. +- Preferred URL launch patterns: + - DOM anchor links with `target="_blank"`. + - `window.open()` in JavaScript context. + - Native URL interception in WebView delegates. + +## Stability Strategy + +- Wrap SDK calls behind readiness gates. +- Centralize bridge constants so command/event renames are isolated. +- Keep fallback path for legacy keys only where backward compatibility is required. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/scenarios/high-level-scenarios.md b/partner-built/zoom-plugin/skills/virtual-agent/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..a2d7e729 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/scenarios/high-level-scenarios.md @@ -0,0 +1,30 @@ +# High-Level Scenarios + +## 1. Website Campaign Entry + +- Use campaign embed snippet to control initial bot routing without code redeploys. +- Use `show()/hide()/open()/close()` for page-specific behavior. + +## 2. Native Mobile Wrapper (Android/iOS) + +- Host campaign URL in WebView/WKWebView. +- Inject customer context (`language`, name, profile fields). +- Route exit and handoff messages into native app state. + +## 3. Bot-to-Agent Escalation + +- Listen for `support_handoff` payload. +- Persist payload to backend for CRM/ticket enrichment. +- Route customer into live support workflow. + +## 4. URL Navigation Governance + +- Open trusted links inside app. +- Send external links to system browser when policy requires. +- Handle `target="_blank"` and `window.open` explicitly. + +## 5. Knowledge-Base Sync Pipeline + +- Use web sync for crawlable documentation. +- Use custom API connector for external CMS pull/push synchronization. +- Re-run sync on content release cadence. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md b/partner-built/zoom-plugin/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..96a48a51 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,40 @@ +# Common Drift and Breaks + +## SDK Not Ready + +Symptoms: +- `window.zoomCampaignSdk` is undefined. + +Fix: +- Register logic only after `zoomCampaignSdk:ready`. +- Prefer `waitForReady()` when available. + +## Campaign Configured but Not Showing + +Checks: +- Confirm campaign targeting includes mobile when using Android/iOS WebView. +- Validate style/config API network responses. + +## Subdomain/Login Failures + +Symptoms: +- Login fails due to subdomain connection issue. + +Fix: +- Verify subdomain allowlist and environment settings in Virtual Agent preferences. + +## Script Tag Loads Inconsistently + +Cause: +- `defer` can break execution order in certain third-party-link flows. + +Fix: +- Remove `defer` or use `async` based on page lifecycle. + +## Deprecated URL Command Usage + +Symptoms: +- Legacy `openURL` command path behaves unpredictably across versions. + +Fix: +- Use DOM links (`target="_blank"`) or `window.open` and explicit native navigation handlers. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/web/SKILL.md b/partner-built/zoom-plugin/skills/virtual-agent/web/SKILL.md new file mode 100644 index 00000000..be78ad7f --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/web/SKILL.md @@ -0,0 +1,39 @@ +--- +name: virtual-agent/web +description: "Zoom Virtual Agent SDK for web embeds. Use for campaign or entry ID chat launch, event-driven controls, user context updates, and CSP-safe deployment." +user-invocable: false +triggers: + - "virtual agent web" + - "zoomCampaignSdk" + - "zcc-sdk" + - "campaign embed" + - "entry id" + - "waitForReady" + - "engagement_started" +--- + +# Zoom Virtual Agent SDK - Web + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/web/ +- https://developers.zoom.us/docs/virtual-agent/web/reference/ + +## Quick Links + +1. [concepts/lifecycle-and-events.md](concepts/lifecycle-and-events.md) +2. [examples/campaign-and-entry-patterns.md](examples/campaign-and-entry-patterns.md) +3. [references/web-reference-map.md](references/web-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Hard Guardrails + +- Gate calls behind readiness (`zoomCampaignSdk:ready` or `waitForReady()`). +- Do not call `show/hide/open/close` before SDK initialization. +- Keep CSP and script host policy validated before debugging business logic. +- Prefer campaign embed over entry ID when minimizing user friction is a priority. + +## Chaining + +- Product-level architecture and drift checks: [../SKILL.md](../SKILL.md) +- Contact Center web context: [../../contact-center/web/SKILL.md](../../contact-center/web/SKILL.md) +- OAuth or REST for backend workflows: [../../oauth/SKILL.md](../../oauth/SKILL.md), [../../rest-api/SKILL.md](../../rest-api/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/virtual-agent/web/concepts/lifecycle-and-events.md b/partner-built/zoom-plugin/skills/virtual-agent/web/concepts/lifecycle-and-events.md new file mode 100644 index 00000000..6040b204 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/web/concepts/lifecycle-and-events.md @@ -0,0 +1,30 @@ +# Lifecycle and Events (Web) + +## Lifecycle + +1. Inject script with API key and env. +2. Wait for `zoomCampaignSdk:ready` or `waitForReady()`. +3. Register event listeners. +4. Execute control calls (`open`, `close`, `show`, `hide`, `endChat`). +5. Optionally refresh user variables via `updateUserContext()`. +6. Remove listeners during page teardown. + +## Event Surface + +- `open` +- `close` +- `show` +- `hide` +- `engagement_started` +- `engagement_ended` + +## Method Surface + +- `close()` +- `endChat()` +- `hide()` +- `show()` +- `ChangeCampaign(id, channel?)` +- `updateUserContext()` +- `waitForInit()` +- `waitForReady()` diff --git a/partner-built/zoom-plugin/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md b/partner-built/zoom-plugin/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md new file mode 100644 index 00000000..e5c39abe --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md @@ -0,0 +1,37 @@ +# Campaign and Entry Patterns + +## Campaign-First Pattern (Recommended) + +```html + + +``` + +## Runtime User Context Refresh + +```javascript +window.zoomCampaignSdkConfig = { + env: 'us01', + apikey: 'YOUR_API_KEY', + firstName: 'Ada', + email: 'ada@example.com' +}; + +window.addEventListener('zoomCampaignSdk:ready', async () => { + if (window.zoomCampaignSdk.waitForReady) { + await window.zoomCampaignSdk.waitForReady(); + } + window.zoomCampaignSdk.updateUserContext(); +}); +``` + +## Entry ID Fallback Pattern + +Use entry ID only when your flow requires pre-chat data collection that cannot be handled in campaign configuration. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/web/references/web-reference-map.md b/partner-built/zoom-plugin/skills/virtual-agent/web/references/web-reference-map.md new file mode 100644 index 00000000..93c9d0b7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/web/references/web-reference-map.md @@ -0,0 +1,14 @@ +# Web Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/web/get-started/ +- Chat embed: https://developers.zoom.us/docs/virtual-agent/web/chat/ +- Campaign controls: https://developers.zoom.us/docs/virtual-agent/web/campaigns/ +- SDK reference: https://developers.zoom.us/docs/virtual-agent/web/reference/ + +## Operational Notes + +- Campaign mode supports central admin routing and lower app-code churn. +- Entry ID mode can increase friction and should be selective. +- Keep script host, CSP, and environment alignment (`us01`/`eu01`) in preflight checks. diff --git a/partner-built/zoom-plugin/skills/virtual-agent/web/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/virtual-agent/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..57209516 --- /dev/null +++ b/partner-built/zoom-plugin/skills/virtual-agent/web/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Web Common Issues + +## `window.zoomCampaignSdk` Is Undefined + +- Confirm script URL is reachable and not blocked. +- Confirm initialization completed before method calls. + +## CSP Blocks SDK + +- Add CSP directives for script/connect/media/font/image paths required by Zoom SDK. +- Re-test with browser console open for wasm or websocket policy errors. + +## Campaign Not Triggering + +- Validate campaign targeting rules and page conditions. +- Inspect network calls and config/style responses. + +## Third-Party Link Behavior + +- Avoid fragile `defer` script behavior when startup triggers external links. +- Prefer explicit handling for `target="_blank"` and `window.open` flows. diff --git a/partner-built/zoom-plugin/skills/webhooks/RUNBOOK.md b/partner-built/zoom-plugin/skills/webhooks/RUNBOOK.md new file mode 100644 index 00000000..5cbf0f00 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/RUNBOOK.md @@ -0,0 +1,83 @@ +# Webhooks 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common webhook failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Endpoint Reachability + +- Public HTTPS endpoint is reachable from Zoom. +- Reverse proxy routes to the correct service path. + +## 2) Confirm Signature Verification + +- Verify `x-zm-signature` with raw request body. +- Use `x-zm-request-timestamp` and reject stale timestamps. +- Do not re-serialize parsed JSON for signature material. + +### Signature Formula Reminder + +```text +payload = "v0:" + x-zm-request-timestamp + ":" + raw_body +expected = "v0=" + HMAC_SHA256(webhook_secret, payload) +``` + +If `raw_body` differs from original bytes (pretty print/re-stringify), verification fails. + +## 3) Confirm URL Validation Handling + +- Handle `endpoint.url_validation` challenge correctly. +- Return expected `plainToken` and computed `encryptedToken` when required. + +### URL Validation Reminder + +On `event = endpoint.url_validation`, hash `payload.plainToken` with your webhook secret and return both values exactly. + +## 4) Confirm Event Subscription Setup + +- Feature/Event subscriptions enabled in app config. +- Required event types selected and saved. + +## 5) Confirm Processing Pattern + +- Respond HTTP 200 quickly. +- Process business logic asynchronously. +- Make handlers idempotent for retries. + +## 6) Quick Probes + +- Local test payload verifies signature path. +- Zoom test event reaches endpoint and is logged. +- No repeated non-200 responses in logs. + +### Copy/Paste Validation Commands + +```bash +# 1) Reachability check (replace with your webhook route) +curl -sS -i "https://your-domain.example/webhook" + +# 2) Check service logs quickly while sending test events +# (replace command with your runtime: pm2/docker/systemd) +pm2 logs your-service --lines 100 + +# 3) Basic endpoint health check if available +curl -sS -i "https://your-domain.example/health" +``` + +Expected: endpoint is reachable over HTTPS, events appear once, and responses are consistently 2xx. + +## 7) Fast Decision Tree + +- **No events received** -> endpoint unreachable or wrong subscription. +- **401 invalid signature** -> raw body mismatch/secret mismatch. +- **Duplicate events** -> no idempotency or delayed responses. + +## 8) Retry and Idempotency Guardrail + +- Treat webhook delivery as at-least-once. +- Deduplicate by event ID/timestamp/resource key before side effects. +- Keep handlers safe to re-run. diff --git a/partner-built/zoom-plugin/skills/webhooks/SKILL.md b/partner-built/zoom-plugin/skills/webhooks/SKILL.md new file mode 100644 index 00000000..d25a21f5 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/SKILL.md @@ -0,0 +1,117 @@ +--- +name: setup-zoom-webhooks +description: Reference skill for Zoom webhooks. Use after routing to an event-driven workflow when implementing subscriptions, signature verification, delivery handling, retries, or event-type selection. +triggers: + - "zoom webhook" + - "webhook signature" + - "x-zm-signature" + - "event subscription" + - "recording completed webhook" +--- + +# /setup-zoom-webhooks + +Background reference for Zoom event delivery over HTTP. Prefer workflow skills first, then use this file for verification, subscription, and delivery details. + +## Prerequisites + +- Zoom app with Event Subscriptions enabled +- HTTPS endpoint to receive webhooks +- Webhook secret token for verification + +> **Need help with authentication?** See the **[zoom-oauth](../oauth/SKILL.md)** skill for OAuth setup. + +## Quick Start + +```javascript +// Express.js webhook handler +const crypto = require('crypto'); + +// Capture raw body for signature verification (avoid re-serializing JSON). +app.use(require('express').json({ + verify: (req, _res, buf) => { req.rawBody = buf; } +})); + +app.post('/webhook', (req, res) => { + // Verify webhook signature + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const body = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(req.body); + const payload = `v0:${timestamp}:${body}`; + const hash = crypto.createHmac('sha256', WEBHOOK_SECRET) + .update(payload).digest('hex'); + + if (signature !== `v0=${hash}`) { + return res.status(401).send('Invalid signature'); + } + + // Handle event + const { event, payload } = req.body; + console.log(`Received: ${event}`); + + res.status(200).send(); +}); +``` + +## Common Events + +| Event | Description | +|-------|-------------| +| `meeting.started` | Meeting has started | +| `meeting.ended` | Meeting has ended | +| `meeting.participant_joined` | Participant joined meeting | +| `recording.completed` | Cloud recording ready | +| `user.created` | New user added | + +## Detailed References + +- **[references/events.md](references/events.md)** - Complete event types reference +- **[references/verification.md](references/verification.md)** - Webhook URL validation +- **[references/subscriptions.md](references/subscriptions.md)** - Event subscriptions API + +## Troubleshooting + +- **[RUNBOOK.md](RUNBOOK.md)** - 5-minute preflight checks before deep debugging +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Signature verification, retries, URL validation + +## Sample Repositories + +### Official (by Zoom) + +| Type | Repository | Stars | +|------|------------|-------| +| Node.js | [webhook-sample](https://github.com/zoom/webhook-sample) | 34 | +| PostgreSQL | [webhook-to-postgres](https://github.com/zoom/webhook-to-postgres) | 5 | +| Go/Fiber | [Go-Webhooks](https://github.com/zoom/Go-Webhooks) | - | +| Header Auth | [zoom-webhook-verification-headers](https://github.com/zoom/zoom-webhook-verification-headers) | - | + +### Community + +| Language | Repository | Description | +|----------|------------|-------------| +| Laravel | [binary-cats/laravel-webhooks](https://github.com/binary-cats/laravel-webhooks) | Laravel webhook handler | +| AWS Lambda | [splunk/zoom-webhook-to-hec](https://github.com/splunk/zoom-webhook-to-hec) | Serverless to Splunk HEC | +| Node.js | [Will4950/zoom-webhook-listener](https://github.com/Will4950/zoom-webhook-listener) | Webhook forwarder | +| Express+Redis | [ojusave/eventSubscriptionPlayground](https://github.com/ojusave/eventSubscriptionPlayground) | Socket.io + Redis | + +### Multi-Language Samples (by tanchunsiong) + +| Language | Repository | +|----------|------------| +| Node.js | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-NodeJS](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-NodeJS) | +| C# | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-ASP.NET-Core-C-](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-ASP.NET-Core-C-) | +| Java | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Java-Spring-Boot](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Java-Spring-Boot) | +| Python | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Python](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Python) | +| PHP | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-PHP](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-PHP) | + +**Full list**: See [general/references/community-repos.md](../general/references/community-repos.md) + +## Resources + +- **Webhook docs**: https://developers.zoom.us/docs/api/webhooks/ +- **Event reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/webhooks/references/environment-variables.md b/partner-built/zoom-plugin/skills/webhooks/references/environment-variables.md new file mode 100644 index 00000000..a3abf0f2 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/references/environment-variables.md @@ -0,0 +1,14 @@ +# Zoom Webhooks Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_WEBHOOK_SECRET` | Yes | HMAC signature verification for webhook payloads | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `WEBHOOK_SECRET_TOKEN` | Alias | Same secret token under alternate naming | Same as above | +| `ZOOM_VERIFICATION_TOKEN` | Legacy only | Legacy endpoint verification | Marketplace legacy field (older app configs) | + +## Notes + +- Prefer `ZOOM_WEBHOOK_SECRET` / Secret Token for current implementations. +- Keep webhook secret in server-side secret storage only. diff --git a/partner-built/zoom-plugin/skills/webhooks/references/events.md b/partner-built/zoom-plugin/skills/webhooks/references/events.md new file mode 100644 index 00000000..ee1aa7fe --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/references/events.md @@ -0,0 +1,75 @@ +# Webhooks - Events + +Complete reference of Zoom webhook events. + +## Overview + +Zoom sends webhook events for various actions across meetings, users, recordings, and more. + +## Meeting Events + +| Event | Description | +|-------|-------------| +| `meeting.created` | Meeting created | +| `meeting.updated` | Meeting updated | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting started | +| `meeting.ended` | Meeting ended | +| `meeting.participant_joined` | Participant joined | +| `meeting.participant_left` | Participant left | +| `meeting.sharing_started` | Screen share started | +| `meeting.sharing_ended` | Screen share ended | + +## Recording Events + +| Event | Description | +|-------|-------------| +| `recording.started` | Recording started | +| `recording.stopped` | Recording stopped | +| `recording.paused` | Recording paused | +| `recording.resumed` | Recording resumed | +| `recording.completed` | Recording ready for download | +| `recording.trashed` | Recording moved to trash | +| `recording.deleted` | Recording permanently deleted | + +## User Events + +| Event | Description | +|-------|-------------| +| `user.created` | User created | +| `user.updated` | User updated | +| `user.deleted` | User deleted | +| `user.activated` | User activated | +| `user.deactivated` | User deactivated | + +## Webinar Events + +| Event | Description | +|-------|-------------| +| `webinar.created` | Webinar created | +| `webinar.updated` | Webinar updated | +| `webinar.deleted` | Webinar deleted | +| `webinar.started` | Webinar started | +| `webinar.ended` | Webinar ended | + +## Event Payload Structure + +```json +{ + "event": "meeting.started", + "event_ts": 1234567890, + "payload": { + "account_id": "account_id", + "object": { + "id": "meeting_id", + "topic": "Meeting Topic", + "host_id": "host_user_id", + "start_time": "2024-01-15T10:00:00Z" + } + } +} +``` + +## Resources + +- **Events reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ diff --git a/partner-built/zoom-plugin/skills/webhooks/references/signature-verification.md b/partner-built/zoom-plugin/skills/webhooks/references/signature-verification.md new file mode 100644 index 00000000..5958a199 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/references/signature-verification.md @@ -0,0 +1,6 @@ +# Signature Verification + +This content lives in `verification.md` (URL validation + request signature verification). + +- See: [verification.md](verification.md) + diff --git a/partner-built/zoom-plugin/skills/webhooks/references/subscriptions.md b/partner-built/zoom-plugin/skills/webhooks/references/subscriptions.md new file mode 100644 index 00000000..79baaeb6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/references/subscriptions.md @@ -0,0 +1,272 @@ +# Webhooks - Subscriptions + +Configure webhook event subscriptions for real-time notifications. + +## Overview + +Subscribe to Zoom events to receive real-time notifications at your endpoint. This enables: +- Real-time meeting tracking (started, ended, participant changes) +- Automated recording processing pipelines +- User lifecycle management +- Skill chaining: Combine REST API calls with event-driven workflows + +## Configuring Subscriptions + +### Method 1: Via Marketplace Portal (Recommended for Setup) + +1. Go to your app in [Marketplace](https://marketplace.zoom.us/) +2. Navigate to **Feature** → **Event Subscriptions** +3. Add subscription name and endpoint URL +4. Select events to subscribe to +5. Save and activate + +### Method 2: Via API (Programmatic Management) + +Use the Webhook Subscriptions API for programmatic subscription management. + +#### Create Subscription + +```bash +POST /webhooks/options +``` + +```json +{ + "notification_endpoint_url": "https://your-server.com/zoom/webhook", + "events": [ + "meeting.started", + "meeting.ended", + "meeting.participant_joined", + "recording.completed" + ] +} +``` + +#### Get Subscription + +```bash +GET /webhooks/options +``` + +**Response:** +```json +{ + "notification_endpoint_url": "https://your-server.com/zoom/webhook", + "events": [ + "meeting.started", + "meeting.ended", + "meeting.participant_joined", + "recording.completed" + ] +} +``` + +#### Update Subscription + +```bash +PATCH /webhooks/options +``` + +```json +{ + "events": [ + "meeting.started", + "meeting.ended", + "recording.completed", + "user.created" + ] +} +``` + +### Subscription Settings + +| Setting | Description | Required | +|---------|-------------|----------| +| **notification_endpoint_url** | Your HTTPS endpoint (must be publicly accessible) | Yes | +| **events** | Array of event types to subscribe to | Yes | + +## Event Categories + +### Meeting Events +| Event | Trigger | +|-------|---------| +| `meeting.created` | Meeting scheduled | +| `meeting.updated` | Meeting settings changed | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins | +| `meeting.participant_left` | Participant leaves | +| `meeting.sharing_started` | Screen share begins | +| `meeting.sharing_ended` | Screen share ends | + +### Recording Events +| Event | Trigger | +|-------|---------| +| `recording.started` | Cloud recording begins | +| `recording.stopped` | Cloud recording paused/stopped | +| `recording.paused` | Cloud recording paused | +| `recording.resumed` | Cloud recording resumed | +| `recording.completed` | Recording processed and available | +| `recording.trashed` | Recording moved to trash | +| `recording.deleted` | Recording permanently deleted | +| `recording.recovered` | Recording restored from trash | + +### User Events +| Event | Trigger | +|-------|---------| +| `user.created` | New user added | +| `user.updated` | User details changed | +| `user.deleted` | User removed | +| `user.deactivated` | User deactivated | +| `user.activated` | User activated | + +### Webinar Events +| Event | Trigger | +|-------|---------| +| `webinar.created` | Webinar scheduled | +| `webinar.started` | Webinar begins | +| `webinar.ended` | Webinar ends | +| `webinar.registration_created` | New registration | + +## Subscription Object Schema + +```json +{ + "notification_endpoint_url": "string (HTTPS URL)", + "events": ["array of event type strings"], + "secret_token": "string (for signature verification)" +} +``` + +## Code Examples + +### JavaScript - Express Webhook Handler with Subscription Check + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const axios = require('axios'); + +const app = express(); +app.use(express.json()); + +// Verify webhook signature +function verifyWebhookSignature(req, secret) { + const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`; + const hashForVerify = crypto + .createHmac('sha256', secret) + .update(message) + .digest('hex'); + + const signature = `v0=${hashForVerify}`; + return signature === req.headers['x-zm-signature']; +} + +// Handle webhook events +app.post('/zoom/webhook', (req, res) => { + const WEBHOOK_SECRET = process.env.ZOOM_WEBHOOK_SECRET; + + if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) { + return res.status(401).send('Unauthorized'); + } + + const { event, payload } = req.body; + + switch (event) { + case 'meeting.started': + console.log(`Meeting started: ${payload.object.topic}`); + break; + case 'meeting.ended': + console.log(`Meeting ended: ${payload.object.uuid}`); + break; + case 'recording.completed': + processRecording(payload.object); + break; + } + + res.status(200).send('OK'); +}); +``` + +### JavaScript - Manage Subscriptions via API + +```javascript +async function updateWebhookSubscription(accessToken, events) { + const response = await axios.patch( + 'https://api.zoom.us/v2/webhooks/options', + { events }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + return response.data; +} + +// Add new events to subscription +await updateWebhookSubscription(token, [ + 'meeting.started', + 'meeting.ended', + 'recording.completed', + 'user.created', // New event + 'user.deleted' // New event +]); +``` + +## Multiple Subscriptions + +You can create multiple subscriptions to: +- Send different events to different endpoints +- Separate production and development endpoints +- Organize by event type +- Route events to different microservices + +## Skill Chaining + +Webhooks are commonly combined with REST API in skill chains: + +| Chain | Pattern | Use Case | +|-------|---------|----------| +| REST API → Webhooks | Create meeting, subscribe to events | Track meeting lifecycle | +| Webhooks → REST API | Receive event, fetch details | Recording download on completion | +| Users API → Webhooks | Create user, subscribe to user events | User lifecycle tracking | + +**Example: Meeting creation with event tracking** +```javascript +// Step 1: Subscribe to meeting events (one-time setup) +await updateWebhookSubscription(token, ['meeting.started', 'meeting.ended']); + +// Step 2: Create meeting via REST API +const meeting = await createMeeting(token, { topic: 'Team Standup', type: 2 }); + +// Step 3: When meeting.started webhook arrives, track it +// Step 4: When meeting.ended webhook arrives, process attendance +``` + +See [meeting-details-with-events.md](../../general/use-cases/meeting-details-with-events.md) for complete skill chaining patterns. + +## Testing Webhooks + +1. **Local development**: Use [ngrok](https://ngrok.com/) to expose local endpoint + ```bash + ngrok http 3000 + ``` +2. **Webhook logs**: Check Marketplace portal → App → Webhooks → Logs +3. **Test endpoint**: Validate signature handling before going live +4. **Retry behavior**: Zoom retries failed webhooks (5xx responses) up to 3 times + +## Required Scopes + +| Scope | Operations | +|-------|------------| +| `webhook:read:admin` | View webhook settings | +| `webhook:write:admin` | Modify webhook settings | + +## Resources + +- **Webhooks overview**: https://developers.zoom.us/docs/api/rest/webhook-reference/ +- **Event types**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Signature verification**: See [signature-verification.md](signature-verification.md) diff --git a/partner-built/zoom-plugin/skills/webhooks/references/verification.md b/partner-built/zoom-plugin/skills/webhooks/references/verification.md new file mode 100644 index 00000000..a0169cb6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/references/verification.md @@ -0,0 +1,94 @@ +# Webhooks - Verification + +Verify webhook authenticity and handle URL validation. + +## Overview + +Zoom provides two verification mechanisms: +1. **URL Validation** - Verify your endpoint during setup +2. **Request Signature** - Verify each webhook request + +## URL Validation + +When you configure a webhook endpoint, Zoom sends a validation request: + +```json +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "random_token_string" + } +} +``` + +### Response Required + +Hash the token with your webhook secret and respond: + +```javascript +const crypto = require('crypto'); + +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'endpoint.url_validation') { + const hashForValidation = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hashForValidation + }); + } + + // Handle other events... + res.status(200).send(); +}); +``` + +## Request Signature Verification + +Verify each webhook request is from Zoom: + +### Headers + +| Header | Description | +|--------|-------------| +| `x-zm-signature` | Request signature | +| `x-zm-request-timestamp` | Request timestamp | + +### Verification Code + +```javascript +const crypto = require('crypto'); + +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + // Prefer raw body bytes captured by your framework to avoid JSON re-serialization mismatches. + const body = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(req.body); + + const message = `v0:${timestamp}:${body}`; + const hash = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + const expectedSignature = `v0=${hash}`; + + return signature === expectedSignature; +} +``` + +## Security Best Practices + +1. Always verify signatures +2. Check timestamp to prevent replay attacks +3. Use HTTPS endpoints only +4. Keep webhook secret secure + +## Resources + +- **Webhook verification**: https://developers.zoom.us/docs/api/rest/webhook-reference/#verify-webhook-events diff --git a/partner-built/zoom-plugin/skills/webhooks/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/webhooks/troubleshooting/common-issues.md new file mode 100644 index 00000000..155da961 --- /dev/null +++ b/partner-built/zoom-plugin/skills/webhooks/troubleshooting/common-issues.md @@ -0,0 +1,34 @@ +# Common Issues + +Quick diagnostics for Zoom Webhooks integrations. + +## Signature Verification Fails (401 / "Invalid signature") + +**Common causes**: +- You are computing the HMAC over a **re-serialized** body (different whitespace/key order). +- You are using the wrong secret (webhook secret vs OAuth secret). +- You are not including the `v0:{timestamp}:{body}` prefix exactly. + +**Fix**: +- Verify signatures using the exact raw request body bytes (capture raw body before JSON parsing). +- Validate both `x-zm-signature` and `x-zm-request-timestamp` and reject stale timestamps (replay protection). + +See: [verification.md](../references/verification.md) + +## Timeouts / Retries / Duplicate Events + +**Symptom**: Zoom retries delivery, you process the same event multiple times. + +**Fix**: +- Respond fast (acknowledge ASAP, then enqueue work). +- Make handlers idempotent (dedupe by event ID/timestamp + payload identifiers). + +## URL Validation Fails + +**Symptom**: You can’t enable the webhook endpoint in Marketplace; validation fails. + +**Fix**: +- Implement `endpoint.url_validation` response correctly (plainToken + encryptedToken). + +See: [verification.md](../references/verification.md) + diff --git a/partner-built/zoom-plugin/skills/websockets/RUNBOOK.md b/partner-built/zoom-plugin/skills/websockets/RUNBOOK.md new file mode 100644 index 00000000..50b3961c --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/RUNBOOK.md @@ -0,0 +1,85 @@ +# WebSockets 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom WebSockets failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm OAuth Token Generation + +- Use S2S credentials and account ID. +- Token endpoint: `https://zoom.us/oauth/token`. +- Refresh token before expiry. + +### Token Sanity Checks + +- Verify token response is JSON and contains `access_token`. +- Record token expiry and refresh proactively. +- If auth intermittently fails, check for clock skew and stale cached tokens. + +## 2) Confirm Subscription Configuration + +- Event subscription created with WebSockets delivery type. +- Required event types selected and saved. + +## 3) Confirm Connection URL and Auth + +- Use exact WebSocket URL from Zoom subscription config. +- Attach access token as required by protocol/headers. + +## 4) Confirm Runtime Reliability + +- Implement reconnect with backoff. +- Handle heartbeat/ping-pong and connection lifecycle events. +- Prevent duplicate consumers if multiple workers run. + +### Minimal Reliability Policy + +- Backoff: exponential with jitter. +- Cap retries and alert after sustained failures. +- Ensure only one active consumer per subscription stream in each environment. + +## 5) Confirm Event Processing Semantics + +- Handle ordering assumptions carefully. +- Make event handlers idempotent. +- Log event IDs and delivery timestamps. + +## 6) Quick Probes + +- Access token request succeeds and returns JSON. +- WebSocket connects and receives at least one subscribed event. +- Reconnect path works after forced disconnect. + +### Copy/Paste Validation Commands + +```bash +# 1) Validate S2S token request +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) Basic Zoom API probe with token +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" + +# 3) Tail app logs while forcing reconnect tests +pm2 logs your-websocket-service --lines 120 +``` + +Expected: token/API probes return JSON; websocket service logs show connect -> receive -> reconnect sequence. + +## 7) Fast Decision Tree + +- **Connection refused/closed** -> token invalid, wrong URL, or subscription config issue. +- **Connected but no events** -> wrong event selection or no triggering activity. +- **Event storms/duplicates** -> missing dedupe/idempotency logic. + +## 8) WebSockets vs Webhooks Guardrail + +- If your use case does not need persistent low-latency delivery, webhook delivery may be simpler to operate. +- Choose WebSockets when you can own connection lifecycle monitoring and reconnect behavior. diff --git a/partner-built/zoom-plugin/skills/websockets/SKILL.md b/partner-built/zoom-plugin/skills/websockets/SKILL.md new file mode 100644 index 00000000..f3de995f --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/SKILL.md @@ -0,0 +1,249 @@ +--- +name: setup-zoom-websockets +description: Reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make WebSockets preferable to webhooks. +triggers: + - "zoom websockets" + - "websocket event subscription" + - "persistent zoom events" + - "low latency zoom events" + - "zoom websocket connection" +--- + +# /setup-zoom-websockets + +Background reference for persistent Zoom event streams. Prefer workflow routing first, then use this file when WebSockets are plausibly better than webhooks. + +## WebSockets vs Webhooks + +| Aspect | WebSockets | Webhooks | +|--------|------------|----------| +| **Connection** | Persistent, bidirectional | One-time HTTP POST | +| **Latency** | Lower (no HTTP overhead) | Higher (new connection per event) | +| **Security** | Direct connection, no exposed endpoint | Requires endpoint validation, IP whitelisting | +| **Model** | Pull (you connect to Zoom) | Push (Zoom connects to you) | +| **State** | Stateful (maintains connection) | Stateless (each event independent) | +| **Setup** | More complex (access token, connection) | Simpler (just endpoint URL) | + +**Choose WebSockets when:** +- Real-time, low-latency updates are critical +- Security is paramount (banking, healthcare, finance) +- You don't want to expose a public endpoint +- You need bidirectional communication + +**Choose Webhooks when:** +- Simpler setup is preferred +- Small number of event notifications +- Existing HTTP infrastructure + +## Prerequisites + +- Server-to-Server OAuth app in [Zoom Marketplace](https://marketplace.zoom.us/) +- Account ID, Client ID, and Client Secret +- WebSocket subscription with events enabled + +> **Need help with S2S OAuth?** See the **[zoom-oauth](../oauth/SKILL.md)** skill for complete authentication flows. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](RUNBOOK.md)** before deep debugging. + +## Quick Start + +### 1. Create Server-to-Server OAuth App + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/develop/create) +2. Create a **Server-to-Server OAuth** app +3. Copy Account ID, Client ID, Client Secret + +### 2. Enable WebSocket Subscription + +1. In your app, go to **Feature** → **Event Subscriptions** +2. Add an Event Subscription +3. Select **WebSockets** as the method type +4. Select events to subscribe to (e.g., `meeting.created`, `meeting.started`) +5. Save - an endpoint URL will be generated + +### 3. Connect via WebSocket + +```javascript +const WebSocket = require('ws'); +const axios = require('axios'); + +// Step 1: Get access token +async function getAccessToken() { + const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data.access_token; +} + +// Step 2: Connect to WebSocket +async function connectWebSocket() { + const accessToken = await getAccessToken(); + + // WebSocket URL from your subscription settings + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${SUBSCRIPTION_ID}&access_token=${accessToken}`; + + const ws = new WebSocket(wsUrl); + + ws.on('open', () => { + console.log('WebSocket connection established'); + }); + + ws.on('message', (data) => { + const event = JSON.parse(data); + console.log('Event received:', event.event); + + // Handle different event types + switch (event.event) { + case 'meeting.started': + console.log(`Meeting started: ${event.payload.object.topic}`); + break; + case 'meeting.ended': + console.log(`Meeting ended: ${event.payload.object.uuid}`); + break; + case 'meeting.participant_joined': + console.log(`Participant joined: ${event.payload.object.participant.user_name}`); + break; + } + }); + + ws.on('close', (code, reason) => { + console.log(`Connection closed: ${code} - ${reason}`); + // Implement reconnection logic + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); + + return ws; +} + +connectWebSocket(); +``` + +## Event Format + +Events received via WebSocket have the same format as webhook events: + +```json +{ + "event": "meeting.started", + "event_ts": 1706123456789, + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Team Standup", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "timezone": "America/Los_Angeles" + } + } +} +``` + +## Common Events + +| Event | Description | +|-------|-------------| +| `meeting.created` | Meeting scheduled | +| `meeting.updated` | Meeting settings changed | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins meeting | +| `meeting.participant_left` | Participant leaves meeting | +| `recording.completed` | Cloud recording ready | +| `user.created` | New user added | +| `user.updated` | User details changed | + +## Connection Management + +### Keep-Alive + +WebSocket connections require periodic heartbeats. Zoom will close idle connections. + +```javascript +// Send ping every 30 seconds +setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + } +}, 30000); +``` + +### Reconnection + +Implement automatic reconnection for reliability: + +```javascript +function connectWithReconnect() { + const ws = connectWebSocket(); + + ws.on('close', () => { + console.log('Connection lost. Reconnecting in 5 seconds...'); + setTimeout(connectWithReconnect, 5000); + }); + + return ws; +} +``` + +### Single Connection Limit + +**Important:** Only ONE WebSocket connection can be open per subscription at a time. Opening a new connection will close the existing one. + +## Detailed References + +- **[references/connection.md](references/connection.md)** - Connection lifecycle, authentication, error handling +- **[references/events.md](references/events.md)** - Complete event types reference + +## Troubleshooting + +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Subscription URL confusion, disconnects, no-events debugging + +## Sample Repositories + +### Official / Community + +| Type | Repository | Description | +|------|------------|-------------| +| Node.js | [just-zoomit/zoom-websockets](https://github.com/just-zoomit/zoom-websockets) | WebSocket sample with S2S OAuth | + +## WebSockets vs RTMS + +Don't confuse WebSockets with RTMS (Realtime Media Streams): + +| Feature | WebSockets | RTMS | +|---------|------------|------| +| **Purpose** | Event notifications | Media streams | +| **Data** | Meeting events, user events | Audio, video, transcripts | +| **Use case** | React to Zoom events | AI/ML, live transcription | +| **Skill** | This skill | **rtms** | + +For real-time audio/video/transcript data, use the **rtms** skill instead. + +## Resources + +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ +- **Webhooks comparison**: https://www.zoom.com/en/blog/a-guide-to-webhooks-and-websockets/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/partner-built/zoom-plugin/skills/websockets/references/connection.md b/partner-built/zoom-plugin/skills/websockets/references/connection.md new file mode 100644 index 00000000..ccfa27d4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/references/connection.md @@ -0,0 +1,435 @@ +# WebSockets - Connection Management + +Detailed guide for managing WebSocket connections to Zoom. + +## Connection Lifecycle + +``` +1. Generate access token (S2S OAuth) + ↓ +2. Open WebSocket connection with token + ↓ +3. Receive events in real-time + ↓ +4. Handle disconnects and reconnect + ↓ +5. Close connection when done +``` + +## Authentication + +WebSocket connections require a valid Server-to-Server OAuth access token. + +### Generate Access Token + +```javascript +const axios = require('axios'); + +async function getAccessToken(accountId, clientId, clientSecret) { + const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: accountId + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return { + accessToken: response.data.access_token, + expiresIn: response.data.expires_in // Usually 3600 seconds (1 hour) + }; +} +``` + +### Token Refresh + +Access tokens expire after 1 hour. Implement token refresh before expiration: + +```javascript +class ZoomWebSocketClient { + constructor(accountId, clientId, clientSecret, subscriptionId) { + this.accountId = accountId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.subscriptionId = subscriptionId; + this.ws = null; + this.tokenExpiry = null; + } + + async refreshTokenIfNeeded() { + const now = Date.now(); + const bufferTime = 5 * 60 * 1000; // 5 minutes before expiry + + if (!this.tokenExpiry || now >= this.tokenExpiry - bufferTime) { + const { accessToken, expiresIn } = await getAccessToken( + this.accountId, this.clientId, this.clientSecret + ); + this.accessToken = accessToken; + this.tokenExpiry = now + (expiresIn * 1000); + + // Reconnect with new token + if (this.ws) { + this.ws.close(); + await this.connect(); + } + } + } + + async connect() { + await this.refreshTokenIfNeeded(); + + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${this.subscriptionId}&access_token=${this.accessToken}`; + this.ws = new WebSocket(wsUrl); + + // Set up event handlers... + } +} +``` + +## Connection URL + +``` +wss://ws.zoom.us/ws?subscriptionId={SUBSCRIPTION_ID}&access_token={ACCESS_TOKEN} +``` + +| Parameter | Description | +|-----------|-------------| +| `subscriptionId` | Your WebSocket subscription ID from Marketplace | +| `access_token` | Valid S2S OAuth access token | + +## Connection Limits + +| Limit | Value | +|-------|-------| +| **Connections per subscription** | 1 (opening new connection closes existing) | +| **Connection timeout** | Varies (implement keep-alive) | +| **Message size** | Check Zoom docs for current limits | + +## Keep-Alive / Heartbeat + +Maintain connection with periodic pings: + +```javascript +class WebSocketManager { + constructor() { + this.ws = null; + this.pingInterval = null; + } + + startHeartbeat() { + // Ping every 30 seconds + this.pingInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.ping(); + console.log('Ping sent'); + } + }, 30000); + } + + stopHeartbeat() { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + } + + connect(url) { + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('Connected'); + this.startHeartbeat(); + }); + + this.ws.on('pong', () => { + console.log('Pong received - connection alive'); + }); + + this.ws.on('close', () => { + this.stopHeartbeat(); + }); + } +} +``` + +## Reconnection Strategy + +Implement exponential backoff for reconnection: + +```javascript +class ReconnectingWebSocket { + constructor(config) { + this.config = config; + this.ws = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 10; + this.baseDelay = 1000; // 1 second + this.maxDelay = 30000; // 30 seconds + } + + async connect() { + try { + const token = await getAccessToken( + this.config.accountId, + this.config.clientId, + this.config.clientSecret + ); + + const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${token.accessToken}`; + + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('Connected successfully'); + this.reconnectAttempts = 0; // Reset on successful connection + }); + + this.ws.on('close', (code, reason) => { + console.log(`Disconnected: ${code} - ${reason}`); + this.scheduleReconnect(); + }); + + this.ws.on('error', (error) => { + console.error('WebSocket error:', error.message); + }); + + this.ws.on('message', (data) => { + this.handleMessage(JSON.parse(data)); + }); + + } catch (error) { + console.error('Connection failed:', error.message); + this.scheduleReconnect(); + } + } + + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + return; + } + + // Exponential backoff with jitter + const delay = Math.min( + this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000, + this.maxDelay + ); + + console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`); + + setTimeout(() => { + this.reconnectAttempts++; + this.connect(); + }, delay); + } + + handleMessage(event) { + // Override this method to handle events + console.log('Event:', event.event, event.payload); + } + + close() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } +} +``` + +## Error Handling + +### Common Error Codes + +| Code | Meaning | Action | +|------|---------|--------| +| 1000 | Normal closure | Clean shutdown | +| 1001 | Going away | Server shutting down, reconnect | +| 1006 | Abnormal closure | Network issue, reconnect | +| 1008 | Policy violation | Check token validity | +| 1011 | Internal error | Server error, retry later | + +### Error Handling Example + +```javascript +ws.on('close', (code, reason) => { + switch (code) { + case 1000: + console.log('Connection closed normally'); + break; + case 1001: + case 1006: + console.log('Connection lost, reconnecting...'); + scheduleReconnect(); + break; + case 1008: + console.log('Auth error - refreshing token'); + refreshTokenAndReconnect(); + break; + default: + console.log(`Unexpected close: ${code} - ${reason}`); + scheduleReconnect(); + } +}); + +ws.on('error', (error) => { + console.error('WebSocket error:', error); + // The 'close' event will follow, handle reconnection there +}); +``` + +## Complete Example + +```javascript +const WebSocket = require('ws'); +const axios = require('axios'); + +class ZoomWebSocketClient { + constructor(config) { + this.config = config; + this.ws = null; + this.accessToken = null; + this.tokenExpiry = null; + this.pingInterval = null; + this.reconnectAttempts = 0; + this.handlers = new Map(); + } + + on(event, handler) { + this.handlers.set(event, handler); + } + + async getAccessToken() { + const credentials = Buffer.from( + `${this.config.clientId}:${this.config.clientSecret}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: this.config.accountId + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + this.accessToken = response.data.access_token; + this.tokenExpiry = Date.now() + (response.data.expires_in * 1000); + + return this.accessToken; + } + + async connect() { + await this.getAccessToken(); + + const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${this.accessToken}`; + + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('WebSocket connected'); + this.reconnectAttempts = 0; + this.startPing(); + this.scheduleTokenRefresh(); + }); + + this.ws.on('message', (data) => { + const event = JSON.parse(data); + const handler = this.handlers.get(event.event); + if (handler) { + handler(event.payload); + } + }); + + this.ws.on('close', (code, reason) => { + console.log(`Disconnected: ${code}`); + this.stopPing(); + if (code !== 1000) { + this.reconnect(); + } + }); + + this.ws.on('error', (error) => { + console.error('Error:', error.message); + }); + } + + startPing() { + this.pingInterval = setInterval(() => { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.ping(); + } + }, 30000); + } + + stopPing() { + if (this.pingInterval) { + clearInterval(this.pingInterval); + } + } + + scheduleTokenRefresh() { + const refreshIn = this.tokenExpiry - Date.now() - 300000; // 5 min before expiry + setTimeout(() => this.refreshToken(), refreshIn); + } + + async refreshToken() { + await this.getAccessToken(); + // Close and reconnect with new token + this.ws?.close(1000); + await this.connect(); + } + + reconnect() { + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); + this.reconnectAttempts++; + console.log(`Reconnecting in ${delay}ms...`); + setTimeout(() => this.connect(), delay); + } + + disconnect() { + this.stopPing(); + this.ws?.close(1000); + } +} + +// Usage +const client = new ZoomWebSocketClient({ + accountId: process.env.ZOOM_ACCOUNT_ID, + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID +}); + +client.on('meeting.started', (payload) => { + console.log(`Meeting started: ${payload.object.topic}`); +}); + +client.on('meeting.ended', (payload) => { + console.log(`Meeting ended: ${payload.object.uuid}`); +}); + +client.on('meeting.participant_joined', (payload) => { + console.log(`Participant joined: ${payload.object.participant.user_name}`); +}); + +client.connect(); +``` + +## Resources + +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ +- **S2S OAuth guide**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ diff --git a/partner-built/zoom-plugin/skills/websockets/references/environment-variables.md b/partner-built/zoom-plugin/skills/websockets/references/environment-variables.md new file mode 100644 index 00000000..1336ccba --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/references/environment-variables.md @@ -0,0 +1,18 @@ +# Zoom WebSockets Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth app identity for WebSocket subscriptions | 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-level token grant for service apps | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_SUBSCRIPTION_ID` | After setup | Persisted subscription identifier for reconnect/resume | Returned by subscription create API response | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` + +## Notes + +- `ZOOM_SUBSCRIPTION_ID` is not from Marketplace UI; your app stores it after calling the subscription API. diff --git a/partner-built/zoom-plugin/skills/websockets/references/events.md b/partner-built/zoom-plugin/skills/websockets/references/events.md new file mode 100644 index 00000000..8d4b0132 --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/references/events.md @@ -0,0 +1,522 @@ +# WebSockets - Event Types + +Complete reference for events available via Zoom WebSockets. + +## Event Structure + +All WebSocket events follow this structure: + +```json +{ + "event": "event.type", + "event_ts": 1706123456789, + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + // Event-specific data + } + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `event` | string | Event type identifier | +| `event_ts` | number | Unix timestamp (milliseconds) | +| `payload.account_id` | string | Zoom account ID | +| `payload.object` | object | Event-specific payload | + +## Meeting Events + +### meeting.created + +Triggered when a meeting is scheduled. + +```json +{ + "event": "meeting.created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "duration": 60, + "timezone": "America/Los_Angeles" + } + } +} +``` + +### meeting.updated + +Triggered when meeting settings are changed. + +```json +{ + "event": "meeting.updated", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "topic": "Updated: Weekly Team Sync" + }, + "old_object": { + "topic": "Weekly Team Sync" + } + } +} +``` + +### meeting.deleted + +Triggered when a meeting is deleted. + +```json +{ + "event": "meeting.deleted", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789" + } + } +} +``` + +### meeting.started + +Triggered when a meeting begins. + +```json +{ + "event": "meeting.started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "timezone": "America/Los_Angeles" + } + } +} +``` + +### meeting.ended + +Triggered when a meeting ends. + +```json +{ + "event": "meeting.ended", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "end_time": "2024-01-25T11:05:00Z", + "duration": 65 + } + } +} +``` + +### meeting.participant_joined + +Triggered when a participant joins the meeting. + +```json +{ + "event": "meeting.participant_joined", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "participant": { + "id": "participant123", + "user_id": "user456", + "user_name": "John Doe", + "email": "john@example.com", + "join_time": "2024-01-25T10:02:00Z" + } + } + } +} +``` + +### meeting.participant_left + +Triggered when a participant leaves the meeting. + +```json +{ + "event": "meeting.participant_left", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe", + "leave_time": "2024-01-25T10:45:00Z", + "leave_reason": "left the meeting" + } + } + } +} +``` + +### meeting.sharing_started + +Triggered when screen sharing begins. + +```json +{ + "event": "meeting.sharing_started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe" + }, + "sharing_details": { + "content": "screen", + "date_time": "2024-01-25T10:15:00Z" + } + } + } +} +``` + +### meeting.sharing_ended + +Triggered when screen sharing ends. + +```json +{ + "event": "meeting.sharing_ended", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe" + } + } + } +} +``` + +## Recording Events + +### recording.started + +Triggered when cloud recording starts. + +```json +{ + "event": "recording.started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "recording_start": "2024-01-25T10:01:00Z" + } + } +} +``` + +### recording.stopped + +Triggered when cloud recording stops (paused or ended). + +```json +{ + "event": "recording.stopped", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "recording_start": "2024-01-25T10:01:00Z", + "recording_end": "2024-01-25T11:00:00Z" + } + } +} +``` + +### recording.completed + +Triggered when cloud recording is processed and ready for download. + +```json +{ + "event": "recording.completed", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "duration": 60, + "total_size": 157286400, + "recording_count": 2, + "recording_files": [ + { + "id": "file123", + "meeting_id": "abcdefgh-1234-5678-abcd-1234567890ab", + "recording_start": "2024-01-25T10:01:00Z", + "recording_end": "2024-01-25T11:00:00Z", + "file_type": "MP4", + "file_size": 104857600, + "download_url": "https://zoom.us/rec/download/...", + "status": "completed" + }, + { + "id": "file124", + "file_type": "TRANSCRIPT", + "file_size": 52428800, + "download_url": "https://zoom.us/rec/download/..." + } + ] + } + } +} +``` + +### recording.trashed + +Triggered when recording is moved to trash. + +### recording.deleted + +Triggered when recording is permanently deleted. + +### recording.recovered + +Triggered when recording is restored from trash. + +## User Events + +### user.created + +Triggered when a new user is added to the account. + +```json +{ + "event": "user.created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "first_name": "Jane", + "last_name": "Smith", + "email": "jane.smith@example.com", + "type": 2, + "created_at": "2024-01-25T09:00:00Z" + } + } +} +``` + +### user.updated + +Triggered when user details are changed. + +```json +{ + "event": "user.updated", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "first_name": "Jane", + "last_name": "Smith-Jones" + }, + "old_object": { + "last_name": "Smith" + } + } +} +``` + +### user.deleted + +Triggered when a user is removed from the account. + +```json +{ + "event": "user.deleted", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "email": "jane.smith@example.com" + } + } +} +``` + +### user.deactivated + +Triggered when a user is deactivated. + +### user.activated + +Triggered when a user is activated. + +## Webinar Events + +### webinar.created + +### webinar.updated + +### webinar.deleted + +### webinar.started + +### webinar.ended + +### webinar.registration_created + +Triggered when someone registers for a webinar. + +```json +{ + "event": "webinar.registration_created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 9876543210, + "uuid": "webinar-uuid-here", + "registrant": { + "id": "registrant123", + "email": "attendee@example.com", + "first_name": "Attendee", + "last_name": "User", + "join_url": "https://zoom.us/w/..." + } + } + } +} +``` + +## Event Handling Example + +```javascript +const eventHandlers = { + // Meeting events + 'meeting.created': (payload) => { + console.log(`New meeting: ${payload.object.topic}`); + notifyCalendarService(payload.object); + }, + + 'meeting.started': (payload) => { + console.log(`Meeting started: ${payload.object.topic}`); + updateMeetingStatus(payload.object.id, 'in_progress'); + }, + + 'meeting.ended': (payload) => { + console.log(`Meeting ended: ${payload.object.uuid}`); + updateMeetingStatus(payload.object.id, 'completed'); + calculateAttendance(payload.object); + }, + + 'meeting.participant_joined': (payload) => { + const { participant } = payload.object; + console.log(`${participant.user_name} joined`); + trackAttendance(payload.object.id, participant); + }, + + // Recording events + 'recording.completed': (payload) => { + console.log(`Recording ready: ${payload.object.topic}`); + downloadRecordings(payload.object.recording_files); + }, + + // User events + 'user.created': (payload) => { + console.log(`New user: ${payload.object.email}`); + sendWelcomeEmail(payload.object); + } +}; + +ws.on('message', (data) => { + const event = JSON.parse(data); + const handler = eventHandlers[event.event]; + + if (handler) { + handler(event.payload); + } else { + console.log(`Unhandled event: ${event.event}`); + } +}); +``` + +## Subscribing to Events + +Configure which events to receive in your Zoom Marketplace app: + +1. Go to **Feature** → **Event Subscriptions** +2. Select **WebSockets** as method type +3. Check the events you want to receive +4. Save the subscription + +**Note:** You can modify subscriptions at any time. Changes take effect immediately. + +## Event Filtering + +If you're receiving too many events, consider: + +1. **Subscribe selectively** - Only subscribe to events you need +2. **Filter in handler** - Drop events that don't match your criteria +3. **Use multiple subscriptions** - Route different events to different handlers + +```javascript +// Filter example: Only process events for specific hosts +ws.on('message', (data) => { + const event = JSON.parse(data); + + // Only process meetings from specific hosts + const allowedHosts = ['host1@example.com', 'host2@example.com']; + + if (event.event.startsWith('meeting.') && + event.payload.object.host_id && + !allowedHosts.includes(getHostEmail(event.payload.object.host_id))) { + return; // Skip this event + } + + processEvent(event); +}); +``` + +## Resources + +- **Event reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ diff --git a/partner-built/zoom-plugin/skills/websockets/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/websockets/troubleshooting/common-issues.md new file mode 100644 index 00000000..b7ec7d6d --- /dev/null +++ b/partner-built/zoom-plugin/skills/websockets/troubleshooting/common-issues.md @@ -0,0 +1,34 @@ +# Common Issues + +Quick diagnostics for Zoom WebSockets event subscriptions. + +## "Where Is the WebSocket URL?" + +**Symptom**: You can’t find a generic `wss://...` endpoint that works for everyone. + +**Reality**: Your connection is parameterized by your subscription (`subscriptionId`) and an access token. + +See: [connection.md](../references/connection.md) + +## Disconnects / Reconnect Loops + +**Common causes**: +- Access token expired (typically ~1 hour). +- Single-connection limit per subscription (a new connection may close the previous one). +- No heartbeat/keep-alive handling in your client. + +**Fix**: +- Refresh token proactively and reconnect with the new token. +- Implement exponential backoff (with jitter). +- Ensure only one active connection per subscription. + +## No Events Received + +**Common causes**: +- Subscribed event topics don’t match what you’re testing. +- App/subscription not enabled or not deployed as required by your account settings. + +**Fix**: +- Confirm topics in Marketplace and generate an event you actually subscribed to. +- Log raw incoming messages and validate parsing. + diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/RUNBOOK.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/RUNBOOK.md new file mode 100644 index 00000000..d7247f3d --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/RUNBOOK.md @@ -0,0 +1,106 @@ +# Zoom Apps SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom Apps integration failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm App Type and Context + +- App type must be Zoom App in Marketplace. +- Confirm expected running context (`inMeeting`, `inMainClient`, `inWebinar`, etc.). + +Context mismatch often looks like missing APIs. + +## 2) Confirm Domain Allowlist + +- Whitelist the exact dev/prod domains used by your app. +- If app panel is blank or refuses to load, domain allowlist is first check. + +### Blank Panel Triage (60s) + +- Confirm app URL is HTTPS and reachable directly in a browser. +- Confirm the exact host is allowlisted in Marketplace (including subdomain differences). +- Confirm no redirect loop (watch network tab for repeated 30x responses). +- Confirm CSP/X-Frame-Options do not block Zoom embedded browser usage. +- Confirm local tunnel URL in app config matches current active tunnel. + +## 3) Confirm In-Client OAuth Setup + +- Use correct redirect/callback handling for Zoom Apps flow. +- Validate state/PKCE handling if implemented. +- Confirm scopes and re-authorize after scope changes. + +## 4) Confirm SDK Capability Usage + +- Call APIs only when supported in current context/capability set. +- Inspect initialization and capability negotiation results. + +### Capability Probe Snippet + +Use this early in app startup to avoid calling unavailable APIs: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function probeSdk() { + const config = await zoomSdk.config({ + capabilities: [ + 'getSupportedJsApis', + 'getRunningContext', + 'authorize', + 'openUrl', + 'shareApp', + ], + }); + + console.log('runningContext:', config.runningContext); + console.log('supportedApis:', config.supportedApis || []); + + const supported = new Set(config.supportedApis || []); + if (!supported.has('authorize')) { + console.warn('authorize API unavailable in this context/capability set'); + } +} +``` + +## 5) Confirm Local Development Tunnel + +- Use stable HTTPS tunnel (ngrok or equivalent). +- Update Marketplace config when tunnel URL changes. + +## 6) Quick Probes + +- App loads inside Zoom client without blank panel. +- SDK init succeeds and returns expected capabilities. +- OAuth flow completes and API calls work with granted scopes. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify app URL is reachable and returns HTML +curl -sS -i "$ZOOM_APP_URL" + +# 2) Verify OAuth callback URL is reachable +curl -sS -i "$ZOOM_APP_CALLBACK_URL" + +# 3) Verify backend token/config endpoint returns JSON +curl -sS -i "$ZOOM_APP_BASE_URL/api/config" +``` + +Expected: HTTP 200/3xx and valid HTML/JSON (not generic 404/502 pages). + +## 7) Fast Decision Tree + +- **Blank panel** -> domain allowlist, HTTPS, CSP headers. +- **API unavailable** -> wrong running context or capability not granted. +- **OAuth loop/failure** -> redirect/state/scope mismatch. + +## 8) SDK Selection Guardrail + +- Use **Zoom Apps SDK** when app runs inside Zoom client contexts. +- Use **Meeting SDK** when embedding Zoom meeting UI into your own website/app. +- If you are debugging "missing Zoom Apps APIs" in a standalone browser page, you are likely in the wrong SDK/runtime. diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/SKILL.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/SKILL.md new file mode 100644 index 00000000..273c5c31 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/SKILL.md @@ -0,0 +1,660 @@ +--- +name: zoom-apps-sdk +description: Reference skill for Zoom Apps SDK. Use after routing to an in-client app workflow when building web apps that run inside Zoom meetings, webinars, the main client, or Zoom Phone. +user-invocable: false +triggers: + - "zoom app" + - "in-meeting app" + - "app inside zoom" + - "zoom client app" + - "layers api" + - "immersive mode" + - "camera mode" + - "collaborate mode" + - "appssdk" + - "in-client oauth" + - "zoom mail" + - "domain allowlist" + - "domain whitelist" + - "url whitelisting" + - "blank panel" + - "runningContext" + - "zoomSdk" +--- + +# Zoom Apps SDK + +Background reference for web apps that run inside the Zoom client. Prefer `choose-zoom-approach` first, then route here for Layers API, Collaborate Mode, in-client OAuth, and runtime constraints. + +# Zoom Apps SDK + +Build web apps that run inside the Zoom client - meetings, webinars, main client, and Zoom Phone. + +**Official Documentation**: https://developers.zoom.us/docs/zoom-apps/ +**SDK Reference**: https://appssdk.zoom.us/ +**NPM Package**: https://www.npmjs.com/package/@zoom/appssdk + +## Quick Links + +**New to Zoom Apps? Follow this path:** + +1. **[Architecture](concepts/architecture.md)** - Frontend/backend pattern, embedded browser, deep linking +2. **[Quick Start](examples/quick-start.md)** - Complete working Express + SDK app +3. **[Running Contexts](concepts/running-contexts.md)** - Where your app runs (inMeeting, inMainClient, etc.) +4. **[Zoom Apps vs Meeting SDK](concepts/meeting-sdk-vs-zoom-apps.md)** - Stop mixing app types +4. **[In-Client OAuth](examples/in-client-oauth.md)** - Seamless authorization with PKCE +5. **[API Reference](references/apis.md)** - 100+ SDK methods +6. **Integrated Index** - see the section below in this file +7. **[5-Minute Runbook](RUNBOOK.md)** - Preflight checks before deep debugging + +**Reference:** +- **[API Reference](references/apis.md)** - All SDK methods by category +- **[Events Reference](references/events.md)** - All SDK event listeners +- **[Layers API](references/layers-api.md)** - Immersive and camera mode rendering +- **[OAuth Reference](references/oauth.md)** - OAuth flows for Zoom Apps +- **[Zoom Mail](references/zmail-sdk.md)** - Mail plugin integration + +**Having issues?** +- App won't load in Zoom → Check [Domain Allowlist](#url-whitelisting-required) below +- SDK errors → [Common Issues](troubleshooting/common-issues.md) +- Local dev setup → [Debugging Guide](troubleshooting/debugging.md) +- Version upgrade → [Migration Guide](troubleshooting/migration.md) +- Forum-derived FAQs → [Forum Top Questions](troubleshooting/forum-top-questions.md) + +**Building immersive experiences?** +- [Layers Immersive Mode](examples/layers-immersive.md) - Custom video layouts +- [Camera Mode](examples/layers-camera.md) - Virtual camera overlays + +> **Need help with OAuth?** See the **[zoom-oauth](../oauth/SKILL.md)** skill for authentication flows. + +## SDK Overview + +The Zoom Apps SDK (`@zoom/appssdk`) provides JavaScript APIs for web apps running in Zoom's embedded browser: + +- **Context APIs** - Get meeting, user, and participant info +- **Meeting Actions** - Share app, invite participants, open URLs +- **Authorization** - In-Client OAuth with PKCE (no browser redirect) +- **Layers API** - Immersive video layouts and camera mode overlays +- **Collaborate Mode** - Shared app state across participants +- **App Communication** - Message passing between app instances (main client <-> meeting) +- **Media Controls** - Virtual backgrounds, camera listing, recording control +- **UI Controls** - Expand app, notifications, popout +- **Events** - React to meeting state, participants, sharing, and more + +## Prerequisites + +- Zoom app configured as **"Zoom App"** type in [Marketplace](https://marketplace.zoom.us/) +- OAuth credentials (Client ID + Secret) with Zoom Apps scopes +- Web application (Node.js + Express recommended) +- **Your domain whitelisted** in Marketplace domain allowlist +- ngrok or HTTPS tunnel for local development +- Node.js 18+ (for the backend server) + +## Quick Start + +### Option A: NPM (Recommended for frameworks) + +```bash +npm install @zoom/appssdk +``` + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + try { + const configResponse = await zoomSdk.config({ + capabilities: [ + 'shareApp', + 'getMeetingContext', + 'getUserContext', + 'openUrl' + ], + version: '0.16' + }); + + console.log('Running context:', configResponse.runningContext); + // 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... + + const context = await zoomSdk.getMeetingContext(); + console.log('Meeting ID:', context.meetingID); + } catch (error) { + console.error('Not running inside Zoom:', error.message); + showDemoMode(); + } +} +``` + +### Option B: CDN (Vanilla JS) + +```html + + + +``` + +## Critical: Global Variable Conflict + +The CDN script defines `window.zoomSdk` globally. **Do NOT redeclare it:** + +```javascript +// WRONG - causes SyntaxError in Zoom's embedded browser +let zoomSdk = null; +zoomSdk = window.zoomSdk; + +// CORRECT - use different variable name +let sdk = window.zoomSdk; + +// ALSO CORRECT - NPM import (no conflict) +import zoomSdk from '@zoom/appssdk'; +``` + +This only applies to the CDN approach. The NPM import creates a module-scoped variable, no conflict. + +## Browser Preview / Demo Mode + +The SDK only functions inside the Zoom client. When accessed in a regular browser: +- `window.zoomSdk` exists but `sdk.config()` throws an error +- Always implement try/catch with fallback UI +- Add timeout (3 seconds) in case SDK hangs + +## URL Whitelisting (Required) + +**Your app will NOT load in Zoom unless the domain is whitelisted.** + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Open your app -> **Feature** tab +3. Under **Zoom App**, find **Add Allow List** +4. Add your domain (e.g., `yourdomain.com` for production, `xxxxx.ngrok.io` for dev) + +Without this, the Zoom client shows a blank panel with no error message. + +## OAuth Scopes (Required) + +Capabilities require matching OAuth scopes enabled in Marketplace: + +| Capability | Required Scope | +|------------|----------------| +| `getMeetingContext` | `zoomapp:inmeeting` | +| `getUserContext` | `zoomapp:inmeeting` | +| `shareApp` | `zoomapp:inmeeting` | +| `openUrl` | `zoomapp:inmeeting` | +| `sendAppInvitation` | `zoomapp:inmeeting` | +| `runRenderingContext` | `zoomapp:inmeeting` | +| `authorize` | `zoomapp:inmeeting` | +| `getMeetingParticipants` | `zoomapp:inmeeting` | + +**To add scopes:** Marketplace -> Your App -> **Scopes** tab -> Add required scopes. + +Missing scopes = capability fails silently or throws error. Users must re-authorize if you add new scopes. + +## Running Contexts + +Your app runs in different surfaces within Zoom. The `configResponse.runningContext` tells you where: + +| Context | Surface | Description | +|---------|---------|-------------| +| `inMeeting` | Meeting sidebar | Most common. Full meeting APIs available | +| `inMainClient` | Main client panel | Home tab. No meeting context APIs | +| `inWebinar` | Webinar sidebar | Host/panelist. Meeting + webinar APIs | +| `inImmersive` | Layers API | Full-screen custom rendering | +| `inCamera` | Camera mode | Virtual camera overlay | +| `inCollaborate` | Collaborate mode | Shared state context | +| `inPhone` | Zoom Phone | Phone call app | +| `inChat` | Team Chat | Chat sidebar | + +See **[Running Contexts](concepts/running-contexts.md)** for context-specific behavior and APIs. + +## SDK Initialization Pattern + +Every Zoom App starts with `config()`: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +const configResponse = await zoomSdk.config({ + capabilities: [ + // List ALL APIs you will use + 'getMeetingContext', + 'getUserContext', + 'shareApp', + 'openUrl', + 'authorize', + 'onAuthorized' + ], + version: '0.16' +}); + +// configResponse contains: +// { +// runningContext: 'inMeeting', +// clientVersion: '5.x.x', +// unsupportedApis: [] // APIs not supported in this client version +// } +``` + +**Rules:** +1. `config()` MUST be called before any other SDK method +2. Only capabilities listed in `config()` are available +3. Capabilities must match OAuth scopes in Marketplace +4. Check `unsupportedApis` for graceful degradation + +## In-Client OAuth (Summary) + +Best UX for authorization - no browser redirect: + +```javascript +// 1. Get code challenge from your backend +const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); + +// 2. Trigger in-client authorization +await zoomSdk.authorize({ codeChallenge, state }); + +// 3. Listen for authorization result +zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + // 4. Send code to backend for token exchange + await fetch('/api/auth/token', { + method: 'POST', + body: JSON.stringify({ code, state }) + }); +}); +``` + +See **[In-Client OAuth Guide](examples/in-client-oauth.md)** for complete implementation. + +## Layers API (Summary) + +Build immersive video layouts and camera overlays: + +```javascript +// Start immersive mode - replaces gallery view +await zoomSdk.runRenderingContext({ view: 'immersive' }); + +// Position participant video feeds +await zoomSdk.drawParticipant({ + participantUUID: 'user-uuid', + x: 0, y: 0, width: 640, height: 480, zIndex: 1 +}); + +// Add overlay images +await zoomSdk.drawImage({ + imageData: canvas.toDataURL(), + x: 0, y: 0, width: 1280, height: 720, zIndex: 0 +}); + +// Exit immersive mode +await zoomSdk.closeRenderingContext(); +``` + +See **[Layers Immersive](examples/layers-immersive.md)** and **[Camera Mode](examples/layers-camera.md)**. + +## Environment Variables + +| Variable | Description | Where to Find | +|----------|-------------|---------------| +| `ZOOM_APP_CLIENT_ID` | App client ID | Marketplace -> App -> App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | App client secret | Marketplace -> App -> App Credentials | +| `ZOOM_APP_REDIRECT_URI` | OAuth redirect URL | Your server URL + `/auth` | +| `SESSION_SECRET` | Cookie signing secret | Generate random string | +| `ZOOM_HOST` | Zoom host URL | `https://zoom.us` (or `https://zoomgov.com`) | + +## Common APIs + +| API | Description | +|-----|-------------| +| `config()` | Initialize SDK, request capabilities | +| `getMeetingContext()` | Get meeting ID, topic, status | +| `getUserContext()` | Get user name, role, participant ID | +| `getRunningContext()` | Get current running context | +| `getMeetingParticipants()` | List participants | +| `shareApp()` | Share app screen with participants | +| `openUrl({ url })` | Open URL in external browser | +| `sendAppInvitation()` | Invite users to open your app | +| `authorize()` | Trigger In-Client OAuth | +| `connect()` | Connect to other app instances | +| `postMessage()` | Send message to connected instances | +| `runRenderingContext()` | Start Layers API (immersive/camera) | +| `expandApp({ action })` | Expand/collapse app panel | +| `showNotification()` | Show notification in Zoom | + +## Complete Documentation Library + +### Core Concepts +- **[Architecture](concepts/architecture.md)** - Frontend/backend pattern, embedded browser, deep linking, X-Zoom-App-Context +- **[Running Contexts](concepts/running-contexts.md)** - All contexts, context-specific APIs, multi-instance communication +- **[Security](concepts/security.md)** - OWASP headers, CSP, cookie security, PKCE, token storage + +### Complete Examples +- **[Quick Start](examples/quick-start.md)** - Hello World Express + SDK app +- **[In-Client OAuth](examples/in-client-oauth.md)** - PKCE authorization flow +- **[Layers Immersive](examples/layers-immersive.md)** - Custom video layouts +- **[Camera Mode](examples/layers-camera.md)** - Virtual camera overlays +- **[Collaborate Mode](examples/collaborate-mode.md)** - Shared state across participants +- **[Guest Mode](examples/guest-mode.md)** - Unauthenticated/authenticated/authorized states +- **[Breakout Rooms](examples/breakout-rooms.md)** - Room detection and cross-room state +- **[App Communication](examples/app-communication.md)** - connect + postMessage between instances + +### Troubleshooting +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics and error codes +- **[Debugging](troubleshooting/debugging.md)** - Local dev, ngrok, browser preview +- **[Migration](troubleshooting/migration.md)** - SDK version upgrade notes + +### References +- **[API Reference](references/apis.md)** - All 100+ SDK methods +- **[Events Reference](references/events.md)** - All SDK event listeners +- **[Layers API Reference](references/layers-api.md)** - Drawing and rendering methods +- **[OAuth Reference](references/oauth.md)** - OAuth flows for Zoom Apps +- **[Zoom Mail](references/zmail-sdk.md)** - Mail plugin integration + +## Sample Repositories + +### Official (by Zoom) + +| Repository | Type | Last Updated | Status | SDK Version | +|-----------|------|-------------|--------|-------------| +| [zoomapps-sample-js](https://github.com/zoom/zoomapps-sample-js) | Hello World (Vanilla JS) | Dec 2025 | Active | ^0.16.26 | +| [zoomapps-advancedsample-react](https://github.com/zoom/zoomapps-advancedsample-react) | Advanced (React + Redis) | Oct 2025 | Active | 0.16.0 | +| [zoomapps-customlayout-js](https://github.com/zoom/zoomapps-customlayout-js) | Layers API | Nov 2023 | Stale | ^0.16.8 | +| [zoomapps-texteditor-vuejs](https://github.com/zoom/zoomapps-texteditor-vuejs) | Collaborate (Vue + Y.js) | Oct 2023 | Stale | ^0.16.7 | +| [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) | Serverless (Firebase) | Aug 2024 | Stale | ^0.16.21 | +| [zoomapps-cameramode-vuejs](https://github.com/zoom/zoomapps-cameramode-vuejs) | Camera Mode | - | - | - | +| [zoomapps-workshop-sample](https://github.com/zoom/zoomapps-workshop-sample) | Workshop | - | - | - | + +**Recommended for new projects:** Use `@zoom/appssdk` version `^0.16.26`. + +### Community + +| Type | Repository | Description | +|------|------------|-------------| +| Library | [harvard-edtech/zaccl](https://github.com/harvard-edtech/zaccl) | Zoom App Complete Connection Library | + +**Full list**: See [general/references/community-repos.md](../general/references/community-repos.md) + +### Learning Path + +1. **Start**: `zoomapps-sample-js` - Simplest, most up-to-date +2. **Advanced**: `zoomapps-advancedsample-react` - Comprehensive (In-Client OAuth, Guest Mode, Collaborate) +3. **Specialized**: Pick based on feature (Layers, Serverless, Camera Mode) + +## Critical Gotchas (From Real Development) + +### 1. Global Variable Conflict +The CDN script defines `window.zoomSdk`. Declaring `let zoomSdk` in your code causes `SyntaxError: redeclaration of non-configurable global property`. Use `let sdk = window.zoomSdk` or the NPM import. + +### 2. Domain Allowlist +Your app URL **must** be in the Marketplace domain allowlist. Without it, Zoom shows a blank panel with no error. Also add `appssdk.zoom.us` and any CDN domains you use. + +### 3. Capabilities Must Be Listed +Only APIs listed in `config({ capabilities: [...] })` are available. Calling an unlisted API throws an error. This is also true for event listeners. + +### 4. SDK Only Works Inside Zoom +`zoomSdk.config()` throws outside the Zoom client. Always wrap in try/catch with browser fallback: +```javascript +try { await zoomSdk.config({...}); } catch { showBrowserPreview(); } +``` + +### 5. ngrok URL Changes +Free ngrok URLs change on restart. You must update 4 places in Marketplace: Home URL, Redirect URL, OAuth Allow List, Domain Allow List. Consider ngrok paid plan for stable subdomain. + +### 6. In-Client OAuth vs Web OAuth +Use `zoomSdk.authorize()` (In-Client) for best UX - no browser redirect. Only fall back to web redirect for initial install from Marketplace. + +### 7. Camera Mode CEF Race Condition +Camera mode uses CEF which takes time to initialize. `drawImage`/`drawWebView` may fail if called too early. Implement retry with exponential backoff. + +### 8. Cookie Configuration +Zoom's embedded browser requires cookies with `SameSite=None` and `Secure=true`. Without this, sessions break silently. + +### 9. State Validation +Always validate the OAuth `state` parameter to prevent CSRF attacks. Generate cryptographically random state, store it, and verify on callback. + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/zoom-apps/ +- **SDK reference**: https://appssdk.zoom.us/ +- **NPM package**: https://www.npmjs.com/package/@zoom/appssdk +- **Developer forum**: https://devforum.zoom.us/ +- **GitHub SDK source**: https://github.com/zoom/appssdk + +--- + +**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 Zoom Apps, follow this order:** + +1. **Run preflight checks first** -> [RUNBOOK.md](RUNBOOK.md) + +2. **Read the architecture** -> [concepts/architecture.md](concepts/architecture.md) + - Frontend/backend pattern, embedded browser, deep linking + - Understand how Zoom loads and communicates with your app + +3. **Build your first app** -> [examples/quick-start.md](examples/quick-start.md) + - Complete Express + SDK Hello World + - ngrok setup for local development + +4. **Understand running contexts** -> [concepts/running-contexts.md](concepts/running-contexts.md) + - Where your app runs (inMeeting, inMainClient, inWebinar, etc.) + - Context-specific APIs and limitations + +5. **Implement OAuth** -> [examples/in-client-oauth.md](examples/in-client-oauth.md) + - In-Client OAuth with PKCE (best UX) + - Token exchange and storage + +6. **Add features** -> [references/apis.md](references/apis.md) + - 100+ SDK methods organized by category + - Code examples for each + +7. **Troubleshoot** -> [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Quick diagnostics for common problems + +--- + +## Documentation Structure + +``` +zoom-apps-sdk/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── architecture.md # Frontend/backend, embedded browser, OAuth flow +│ ├── running-contexts.md # Where your app runs + context-specific APIs +│ └── security.md # OWASP headers, CSP, data access layers +│ +├── examples/ # Complete working code +│ ├── quick-start.md # Hello World - minimal Express + SDK app +│ ├── in-client-oauth.md # In-Client OAuth with PKCE +│ ├── layers-immersive.md # Layers API - immersive mode (custom layouts) +│ ├── layers-camera.md # Layers API - camera mode (virtual camera) +│ ├── collaborate-mode.md # Collaborate mode (shared state) +│ ├── guest-mode.md # Guest mode (unauthenticated -> authorized) +│ ├── breakout-rooms.md # Breakout room integration +│ └── app-communication.md # connect + postMessage between instances +│ +├── troubleshooting/ # Problem solving guides +│ ├── common-issues.md # Quick diagnostics, error codes +│ ├── debugging.md # Local dev setup, ngrok, browser preview +│ └── migration.md # SDK version migration notes +│ +└── references/ # Reference documentation + ├── apis.md # Complete API reference (100+ methods) + ├── events.md # All SDK events + ├── layers-api.md # Layers API detailed reference + ├── oauth.md # OAuth flows for Zoom Apps + └── zmail-sdk.md # Zoom Mail integration +``` + +--- + +## By Use Case + +### I want to build a basic Zoom App +1. [Architecture](concepts/architecture.md) - Understand the pattern +2. [Quick Start](examples/quick-start.md) - Build Hello World +3. [In-Client OAuth](examples/in-client-oauth.md) - Add authorization +4. [Security](concepts/security.md) - Required headers + +### I want immersive video layouts (Layers API) +1. [Layers Immersive](examples/layers-immersive.md) - Custom video positions +2. [Layers API Reference](references/layers-api.md) - All drawing methods +3. [App Communication](examples/app-communication.md) - Sync layout across participants + +### I want a virtual camera overlay +1. [Camera Mode](examples/layers-camera.md) - Camera mode rendering +2. [Layers API Reference](references/layers-api.md) - Drawing methods + +### I want real-time collaboration +1. [Collaborate Mode](examples/collaborate-mode.md) - Shared state APIs +2. [App Communication](examples/app-communication.md) - Instance messaging + +### I want guest/anonymous access +1. [Guest Mode](examples/guest-mode.md) - Three authorization states +2. [In-Client OAuth](examples/in-client-oauth.md) - promptAuthorize flow + +### I want breakout room support +1. [Breakout Rooms](examples/breakout-rooms.md) - Room detection and state sync + +### I want to sync between main client and meeting +1. [App Communication](examples/app-communication.md) - connect + postMessage +2. [Running Contexts](concepts/running-contexts.md) - Multi-instance behavior + +### I want serverless deployment +1. [Quick Start](examples/quick-start.md) - Understand the base pattern first +2. Sample: [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) - Firebase pattern + +### I want to add Zoom Mail integration +1. [Zoom Mail Reference](references/zmail-sdk.md) - REST API + mail plugins + +### I'm getting errors +1. [Common Issues](troubleshooting/common-issues.md) - Quick diagnostic table +2. [Debugging](troubleshooting/debugging.md) - Local dev setup, DevTools +3. [Migration](troubleshooting/migration.md) - Version compatibility + +--- + +## Most Critical Documents + +### 1. Architecture (FOUNDATION) +**[concepts/architecture.md](concepts/architecture.md)** + +Understand how Zoom Apps work: Frontend in embedded browser, backend for OAuth/API, SDK as the bridge. Without this, nothing else makes sense. + +### 2. Quick Start (FIRST APP) +**[examples/quick-start.md](examples/quick-start.md)** + +Complete working code. Get something running before diving into advanced features. + +### 3. Common Issues (MOST COMMON PROBLEMS) +**[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** + +90% of Zoom Apps issues are: domain allowlist, global variable conflict, or missing capabilities. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Global Variable Conflict is the #1 Gotcha** + - CDN script defines `window.zoomSdk` globally + - `let zoomSdk = ...` causes SyntaxError in Zoom's browser + - Use `let sdk = window.zoomSdk` or NPM import + +2. **Domain Allowlist is Non-Negotiable** + - App shows blank panel with zero error if domain not whitelisted + - Must include your domain AND `appssdk.zoom.us` AND any CDN domains + - ngrok URLs change on restart - must update Marketplace each time + +3. **config() Gates Everything** + - Must be called first, must list all capabilities + - Unlisted capabilities throw errors + - Check `unsupportedApis` for client version compatibility + +4. **In-Client OAuth > Web OAuth for UX** + - `authorize()` keeps user in Zoom (no browser redirect) + - Web redirect only needed for initial Marketplace install + - Always implement PKCE (code_verifier + code_challenge) + +5. **Two App Instances Can Run Simultaneously** + - Main client instance + meeting instance + - Use `connect()` + `postMessage()` to sync between them + - Pre-meeting setup in main client, use in meeting + +6. **Camera Mode Has CEF Quirks** + - CEF initialization takes time + - Draw calls may fail if too early + - Use retry with exponential backoff + +7. **Cookie Settings Matter** + - `SameSite=None` + `Secure=true` required + - Without this, sessions silently fail in embedded browser + +--- + +## Quick Reference + +### "App shows blank panel" +-> [Domain Allowlist](troubleshooting/common-issues.md) - add domain to Marketplace + +### "SyntaxError: redeclaration" +-> [Global Variable](troubleshooting/common-issues.md) - use `let sdk = window.zoomSdk` + +### "config() throws error" +-> [Browser Preview](troubleshooting/debugging.md) - SDK only works inside Zoom + +### "API call fails silently" +-> [OAuth Scopes](troubleshooting/common-issues.md) - add required scopes in Marketplace + +### "How do I implement [feature]?" +-> [API Reference](references/apis.md) - find the method, check capabilities needed + +### "How do I test locally?" +-> [Debugging Guide](troubleshooting/debugging.md) - ngrok + Marketplace config + +--- + +## Document Version + +Based on **@zoom/appssdk v0.16.x** (latest: 0.16.26+) + +--- + +**Happy coding!** + +Start with [Architecture](concepts/architecture.md) to understand the pattern, then [Quick Start](examples/quick-start.md) to build your first app. diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/architecture.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/architecture.md new file mode 100644 index 00000000..269f52a0 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/architecture.md @@ -0,0 +1,222 @@ +# Zoom Apps Architecture + +## Overview + +A Zoom App is a web application that runs inside the Zoom client's embedded browser. It consists of two parts: + +- **Frontend**: Your web app loaded inside Zoom (HTML/CSS/JS + `@zoom/appssdk`) +- **Backend**: Your server handling OAuth, REST API calls, and business logic + +The SDK (`@zoom/appssdk`) is the bridge between your frontend and the Zoom client. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────┐ +│ ZOOM CLIENT │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Embedded Browser (WebView) │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────┐ │ │ +│ │ │ YOUR FRONTEND WEB APP │ │ │ +│ │ │ │ │ │ +│ │ │ import zoomSdk from '@zoom/appssdk' │ │ │ +│ │ │ zoomSdk.config({...}) │ │ │ +│ │ │ zoomSdk.getMeetingContext() │ │ │ +│ │ │ │ │ │ +│ │ │ fetch('/api/data') ──────────────────────── YOUR BACKEND +│ │ └─────────────────────────────────────────┘ │ │ (Express/Node.js) +│ │ │ │ │ - OAuth token exchange +│ │ │ SDK Bridge │ │ - REST API calls +│ │ ▼ │ │ - Business logic +│ │ Zoom Client APIs │ │ - Token storage +│ │ (meeting, user, UI) │ │ +│ └──────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +## Embedded Browser Details + +Zoom uses different browser engines per platform: + +| Platform | Browser Engine | Notes | +|----------|---------------|-------| +| Windows | WebView2 (Chromium) | Modern, good DevTools | +| macOS | WKWebView (WebKit) | Safari-like behavior | +| iOS | WKWebView | Mobile viewport | +| Android | WebView | Mobile viewport | +| Some surfaces | CEF (Chromium Embedded) | Camera mode uses this | + +**Limitations:** +- No browser extensions +- Limited `window.open` support (use `zoomSdk.openUrl()` instead) +- No access to browser-level storage across different apps +- CSP must allow `frame-ancestors zoom.us *.zoom.us` +- Cookies require `SameSite=None; Secure` + +## App Lifecycle + +### Initial Install (Web OAuth) + +``` +User clicks "Add" in Marketplace + │ + ▼ +Browser opens Zoom OAuth page +(https://zoom.us/oauth/authorize?client_id=...&code_challenge=...) + │ + ▼ +User clicks "Allow" + │ + ▼ +Zoom redirects to your redirect URI with ?code=... + │ + ▼ +Your backend exchanges code + code_verifier for access_token + │ + ▼ +Backend calls GET /v2/zoomapp/deeplink with access_token + │ + ▼ +Backend redirects user to deeplink URL + │ + ▼ +Zoom client opens, loads your frontend URL in embedded browser + │ + ▼ +Frontend calls zoomSdk.config({...}) + │ + ▼ +App is ready +``` + +### Subsequent Opens (In-Client OAuth) + +``` +User opens your app in Zoom client + │ + ▼ +Zoom loads your frontend URL in embedded browser + │ + ▼ +Frontend calls zoomSdk.config({...}) + │ + ▼ +Frontend calls zoomSdk.authorize({ codeChallenge, state }) + │ + ▼ +User approves in Zoom popup (no browser redirect) + │ + ▼ +onAuthorized event fires with authorization code + │ + ▼ +Frontend sends code to backend + │ + ▼ +Backend exchanges code + code_verifier for tokens + │ + ▼ +App is authorized +``` + +## Deep Linking + +After web-based OAuth, your backend must get a deeplink to open the app in Zoom: + +```javascript +// After token exchange, get deeplink +const response = await fetch('https://api.zoom.us/v2/zoomapp/deeplink', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ action: '' }) +}); + +const { deeplink } = await response.json(); +// deeplink = 'zoommtg://zoom.us/...' or similar + +// Redirect user to open in Zoom client +res.redirect(deeplink); +``` + +## X-Zoom-App-Context Header + +When Zoom loads your frontend, it sends an `X-Zoom-App-Context` HTTP header. This encrypted header contains user and meeting context, allowing your backend to identify the user without OAuth. + +### Decryption (Node.js) + +```javascript +const crypto = require('crypto'); + +function decryptContext(header, clientSecret) { + const buf = Buffer.from(header, 'base64'); + const iv = buf.slice(0, 12); // First 12 bytes = IV + const encryptedData = buf.slice(12, buf.length - 16); // Middle = ciphertext + const tag = buf.slice(buf.length - 16); // Last 16 bytes = auth tag + + const key = crypto.createHash('sha256') + .update(clientSecret) + .digest(); + + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(tag); + + const decrypted = Buffer.concat([ + decipher.update(encryptedData), + decipher.final() + ]); + + return JSON.parse(decrypted.toString()); +} + +// Usage in Express middleware +app.use((req, res, next) => { + const contextHeader = req.headers['x-zoom-app-context']; + if (contextHeader) { + req.zoomContext = decryptContext(contextHeader, process.env.ZOOM_APP_CLIENT_SECRET); + // { uid: '...', aud: '...', iss: 'marketplace.zoom.us', ts: ..., ... } + } + next(); +}); +``` + +The decrypted context contains: +- `uid` - Zoom user ID +- `mid` - Meeting ID (if in meeting) +- `aud` - Your app's client ID +- `iss` - Issuer (`marketplace.zoom.us`) +- `ts` - Timestamp + +## Data Access Layers + +Zoom Apps can access data through three layers: + +| Layer | Method | Data Available | Auth Required | +|-------|--------|----------------|---------------| +| **Contextual** | SDK APIs (`getMeetingContext`, etc.) | Meeting/user/participant info | config() only | +| **Server-side** | REST API (via backend) | Full Zoom API (users, meetings, recordings) | OAuth tokens | +| **Header** | X-Zoom-App-Context header | User identity, meeting context | Client secret | + +## Domain Allowlist + +The Zoom client will **only** load URLs from domains in your app's allowlist. + +**Required domains:** +- Your app domain (e.g., `yourdomain.com`) +- `appssdk.zoom.us` (if using CDN) +- Any CDN domains (fonts, CSS, images) +- Any API domains your frontend calls directly + +**Configure in:** Marketplace -> Your App -> Feature -> Zoom App -> Add Allow List + +Without this, the embedded browser shows a blank panel with no error. + +## Resources + +- **Architecture docs**: https://developers.zoom.us/docs/zoom-apps/architecture/ +- **Data access**: https://developers.zoom.us/docs/zoom-apps/data-access/ +- **X-Zoom-App-Context**: https://developers.zoom.us/docs/zoom-apps/zoom-app-context/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md new file mode 100644 index 00000000..990109f4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md @@ -0,0 +1,34 @@ +--- +title: "Zoom Apps vs Meeting SDK (Common Confusion)" +--- + +# Zoom Apps vs Meeting SDK (Common Confusion) + +Forum pattern: developers try to use Meeting SDK concepts inside Zoom Apps, or vice versa. + +## Zoom Apps (Apps SDK) + +Use when you want a **web app that runs inside the Zoom client**: + +- in-meeting panel +- main client +- webinar contexts +- Layers API (immersive / camera mode) +- collaborate mode (shared state) + +The app runs in Zoom’s embedded browser and interacts with the meeting via `@zoom/appssdk` capabilities. + +## Meeting SDK + +Use when you want to **embed the Zoom meeting experience in your own app** (outside the Zoom client): + +- your website or desktop app hosts the meeting UI +- you handle signature generation server-side + +## The Practical Decision + +- “I want a sidebar app inside Zoom” -> Zoom Apps SDK +- “I want to embed Zoom meeting UI inside my product” -> Meeting SDK + +If the question is “show contents in Zoom App via Meeting SDK”, first clarify which experience they actually want (inside Zoom vs embedded in their product). + diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/running-contexts.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/running-contexts.md new file mode 100644 index 00000000..60f4534c --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/running-contexts.md @@ -0,0 +1,190 @@ +# Running Contexts + +## Overview + +A Zoom App can run in multiple surfaces within the Zoom client. The `runningContext` property returned by `config()` tells you where your app is currently loaded. + +```javascript +const configResponse = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'getUserContext', ...], + version: '0.16' +}); + +console.log(configResponse.runningContext); +// 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... +``` + +## All Running Contexts + +| Context | Surface | Meeting APIs | User APIs | Layers APIs | Notes | +|---------|---------|-------------|-----------|-------------|-------| +| `inMeeting` | Meeting sidebar | Yes | Yes | Yes | Most common context | +| `inMainClient` | Main client panel | No | Yes | No | Home tab, no meeting running | +| `inWebinar` | Webinar sidebar | Yes | Yes | Yes | Host/panelist initially | +| `inImmersive` | Layers full-screen | Limited | Yes | Yes | After runRenderingContext | +| `inCamera` | Camera mode | Limited | Yes | Camera only | Virtual camera overlay | +| `inCollaborate` | Collaborate mode | Yes | Yes | No | Shared state context | +| `inPhone` | Zoom Phone | No | Yes | No | Phone call app surface | +| `inChat` | Team Chat | No | Yes | No | Chat sidebar | + +## Context-Specific Behavior + +### inMeeting (Most Common) + +The primary context. Your app appears as a sidebar panel during a meeting. + +```javascript +if (configResponse.runningContext === 'inMeeting') { + const meeting = await zoomSdk.getMeetingContext(); + console.log('Meeting ID:', meeting.meetingID); + console.log('Topic:', meeting.meetingTopic); + + const user = await zoomSdk.getUserContext(); + console.log('Name:', user.screenName); + console.log('Role:', user.role); // 'host' | 'coHost' | 'attendee' +} +``` + +Available: All meeting APIs, sharing, invitations, breakout rooms, recording. + +### inMainClient + +Your app runs in the main Zoom window (not during a meeting). Used for dashboards, settings, pre-meeting setup. + +```javascript +if (configResponse.runningContext === 'inMainClient') { + // NO meeting APIs available - getMeetingContext() will fail + const user = await zoomSdk.getUserContext(); + console.log('Name:', user.screenName); + + // Can still use connect() to sync with meeting instance later +} +``` + +**Key limitation:** No meeting context, no participants, no sharing. + +### inWebinar + +Similar to inMeeting but for webinars. Initially only available to host and panelists. + +```javascript +if (configResponse.runningContext === 'inWebinar') { + const user = await zoomSdk.getUserContext(); + // role: 'host' | 'panelist' | 'attendee' + if (user.role === 'attendee') { + // Limited functionality for attendees + } +} +``` + +### inImmersive / inCamera + +Layers API contexts. Your app has taken over the video rendering. + +- `inImmersive`: Full-screen custom layout (replaces gallery view) +- `inCamera`: Overlay on user's camera feed + +See [Layers API Reference](../references/layers-api.md) for details. + +## Multiple Instances + +A Zoom App can have **two instances running simultaneously**: + +1. **Main client instance** (`inMainClient`) - Always available +2. **Meeting instance** (`inMeeting`) - Created when user opens app in meeting + +### Instance Communication + +Use `connect()` and `postMessage()` to sync between instances: + +```javascript +// Both instances call connect() +await zoomSdk.connect(); + +// Listen for connection +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Connected to other instance'); +}); + +// Send data to other instance +await zoomSdk.postMessage({ type: 'settings', data: mySettings }); + +// Receive data from other instance +zoomSdk.addEventListener('onMessage', (event) => { + const { type, data } = JSON.parse(event.payload); + if (type === 'settings') { + applySettings(data); + } +}); +``` + +**Pattern: Pre-Meeting Setup** + +``` +Main Client Instance Meeting Instance +───────────────────── ───────────────── +User configures settings --> connect() + listen +Store in state --> onConnect fires + postMessage('getSettings') +onMessage('getSettings') --> +postMessage(settings) --> onMessage(settings) + Apply settings to meeting +``` + +## Detecting Context at Runtime + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + const config = await zoomSdk.config({ + capabilities: [ + 'getMeetingContext', 'getUserContext', 'getRunningContext', + 'connect', 'postMessage', 'onConnect', 'onMessage', + 'shareApp', 'runRenderingContext' + ], + version: '0.16' + }); + + switch (config.runningContext) { + case 'inMeeting': + case 'inWebinar': + initMeetingUI(); + break; + case 'inMainClient': + initDashboardUI(); + break; + case 'inImmersive': + case 'inCamera': + initLayersUI(); + break; + default: + initFallbackUI(); + } +} +``` + +## Checking API Availability + +Not all APIs are available in all contexts. Use `getSupportedJsApis()` to check: + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); + +if (supportedApis.includes('authorize')) { + // In-Client OAuth is available + showAuthButton(); +} + +if (supportedApis.includes('runRenderingContext')) { + // Layers API is available + showLayersButton(); +} +``` + +Also check `configResponse.unsupportedApis` after `config()` for capabilities that were requested but not available in the current client version. + +## Resources + +- **Running contexts docs**: https://developers.zoom.us/docs/zoom-apps/guides/in-client-experience/ +- **Instance communication**: https://developers.zoom.us/docs/zoom-apps/guides/collaborate-mode/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/security.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/security.md new file mode 100644 index 00000000..451a32a4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/concepts/security.md @@ -0,0 +1,157 @@ +# Security + +## Overview + +Zoom Apps must meet security requirements for Marketplace approval. This covers required headers, CSP configuration, cookie security, and token storage. + +## Required OWASP Headers + +Zoom's security review requires these HTTP headers on all responses: + +| Header | Required Value | Purpose | +|--------|---------------|---------| +| `Strict-Transport-Security` | `max-age=31536000` | Force HTTPS for 1 year | +| `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing | +| `Content-Security-Policy` | `frame-ancestors 'self' zoom.us *.zoom.us` | Allow Zoom to embed your app | +| `Referrer-Policy` | `same-origin` | Limit referrer info | +| `X-Frame-Options` | `ALLOW-FROM zoom.us` | Legacy frame control | + +### Express Implementation + +```javascript +app.use((req, res, next) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Content-Security-Policy', + "frame-ancestors 'self' zoom.us *.zoom.us" + ); + res.setHeader('Referrer-Policy', 'same-origin'); + next(); +}); +``` + +**Critical:** The `frame-ancestors` CSP directive is what allows Zoom's embedded browser to load your app. Without it, the browser blocks the frame. + +## TLS Requirements + +- HTTPS required for all endpoints (minimum TLS 1.2) +- HTTP redirects to HTTPS +- Valid SSL certificate (self-signed will fail) +- ngrok provides HTTPS automatically for local development + +## Cookie Security + +Zoom's embedded browser requires specific cookie settings: + +```javascript +// Express cookie-session example +app.use(require('cookie-session')({ + name: 'session', + keys: [process.env.SESSION_SECRET], + maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: 'none', // REQUIRED - Zoom embeds your app cross-origin + secure: true // REQUIRED - SameSite=None requires Secure +})); +``` + +**Why `SameSite=None`?** Your app runs inside Zoom's embedded browser, which is a different origin. Without `SameSite=None`, the browser won't send cookies to your server, and sessions break silently. + +## PKCE OAuth Security + +All Zoom Apps OAuth flows should use PKCE (Proof Key for Code Exchange): + +```javascript +const crypto = require('crypto'); + +// Generate PKCE pair +const verifier = crypto.randomBytes(32).toString('hex'); +const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + +// Store verifier in session (server-side) +req.session.codeVerifier = verifier; + +// Send challenge to frontend (or include in OAuth redirect URL) +res.json({ codeChallenge: challenge, state: req.session.state }); +``` + +PKCE prevents authorization code interception attacks. The `code_verifier` never leaves your server. + +## Token Storage + +**Never store tokens in the frontend (localStorage, sessionStorage, cookies).** + +| Storage | When to Use | Security | +|---------|-------------|----------| +| **Redis** | Multi-instance servers, production | Fast, TTL support, scalable | +| **Encrypted session** | Single server, simple apps | Tied to server process | +| **Firestore/DynamoDB** | Serverless (Firebase/Lambda) | Persistent, managed | +| **Database (encrypted)** | Complex apps with user accounts | Full control, encrypted at rest | + +```javascript +// Redis token storage example +const Redis = require('ioredis'); +const redis = new Redis(process.env.REDIS_URL); + +async function storeTokens(zoomUserId, tokens) { + await redis.set( + `zoom:tokens:${zoomUserId}`, + JSON.stringify(tokens), + 'EX', tokens.expires_in // Auto-expire with token + ); +} + +async function getTokens(zoomUserId) { + const data = await redis.get(`zoom:tokens:${zoomUserId}`); + return data ? JSON.parse(data) : null; +} +``` + +## State Parameter (CSRF Protection) + +Always validate the OAuth `state` parameter: + +```javascript +const crypto = require('crypto'); + +// Generate state before OAuth redirect +const state = crypto.randomBytes(16).toString('hex'); +req.session.oauthState = state; + +// Validate state on callback +app.get('/auth', (req, res) => { + if (req.query.state !== req.session.oauthState) { + return res.status(403).send('Invalid state - possible CSRF attack'); + } + // Proceed with token exchange +}); +``` + +## Data Access Layers + +| Layer | What You Access | Authorization | Risk Level | +|-------|----------------|---------------|------------| +| **SDK (contextual)** | Meeting context, user info, UI controls | config() capabilities | Low - scoped to current context | +| **REST API (server-side)** | Full Zoom API (users, meetings, recordings) | OAuth access tokens | Medium - broader data access | +| **X-Zoom-App-Context** | User identity, meeting info | Client secret decryption | Low - read-only identity | + +**Principle of least privilege:** Only request the OAuth scopes and SDK capabilities you actually need. + +## Marketplace Security Review Checklist + +- [ ] All OWASP headers set on every response +- [ ] HTTPS enforced (no HTTP endpoints) +- [ ] PKCE used for OAuth flows +- [ ] State parameter validated on OAuth callbacks +- [ ] Tokens stored server-side (never in frontend) +- [ ] Token refresh implemented (tokens expire in 1 hour) +- [ ] `SameSite=None; Secure` on cookies +- [ ] CSP allows `frame-ancestors zoom.us *.zoom.us` +- [ ] No sensitive data logged to console in production +- [ ] Environment variables for all secrets (no hardcoded credentials) + +## Resources + +- **Security docs**: https://developers.zoom.us/docs/zoom-apps/security/ +- **OWASP headers**: https://developers.zoom.us/docs/zoom-apps/security/owasp/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/app-communication.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/app-communication.md new file mode 100644 index 00000000..3fd2c0dd --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/app-communication.md @@ -0,0 +1,152 @@ +# App Instance Communication + +Sync data between main client and meeting instances using connect() + postMessage(). + +## Overview + +A Zoom App can run two instances simultaneously: +- **Main client instance** (`inMainClient`) - for settings, dashboards +- **Meeting instance** (`inMeeting`) - for in-meeting features + +Use `connect()` and `postMessage()` to pass data between them. + +## Basic Setup + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage'], + version: '0.16' +}); + +// Both instances must call connect() +await zoomSdk.connect(); + +// Know when the other instance connects +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Other instance connected'); +}); + +// Send a message +await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'greeting', data: 'Hello from this instance!' }) +}); + +// Receive messages +zoomSdk.addEventListener('onMessage', (event) => { + const message = JSON.parse(event.payload); + console.log('Received:', message.type, message.data); +}); +``` + +## Pattern: Pre-Meeting Settings + +Configure settings in the main client, use them during the meeting. + +### Main Client Instance + +```javascript +// Running in inMainClient context +const config = await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage', 'getRunningContext'], + version: '0.16' +}); + +if (config.runningContext === 'inMainClient') { + await zoomSdk.connect(); + + // User configures settings + const settings = { + theme: 'dark', + language: 'en', + notifications: true + }; + + // When meeting instance asks for settings, send them + zoomSdk.addEventListener('onMessage', async (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'request-settings') { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'settings', data: settings }) + }); + } + }); +} +``` + +### Meeting Instance + +```javascript +// Running in inMeeting context +if (config.runningContext === 'inMeeting') { + await zoomSdk.connect(); + + let settings = null; + + zoomSdk.addEventListener('onMessage', (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'settings') { + settings = msg.data; + applySettings(settings); + } + }); + + // Request settings from main client instance + zoomSdk.addEventListener('onConnect', async () => { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'request-settings' }) + }); + }); +} +``` + +## Message Protocol + +Define a consistent message format: + +```javascript +// Message types +const MessageType = { + REQUEST_SETTINGS: 'request-settings', + SETTINGS: 'settings', + STATE_UPDATE: 'state-update', + ACTION: 'action' +}; + +// Send helper +async function send(type, data = {}) { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type, data, timestamp: Date.now() }) + }); +} + +// Receive handler +zoomSdk.addEventListener('onMessage', (event) => { + const { type, data } = JSON.parse(event.payload); + + switch (type) { + case MessageType.REQUEST_SETTINGS: + send(MessageType.SETTINGS, currentSettings); + break; + case MessageType.STATE_UPDATE: + applyState(data); + break; + case MessageType.ACTION: + handleAction(data); + break; + } +}); +``` + +## Important Notes + +- Messages are JSON strings (must stringify/parse) +- Both instances must call `connect()` independently +- `onConnect` fires when the other instance connects +- Messages are one-to-one (not broadcast to all participants) +- If one instance isn't running, messages are lost (no queue) + +## Resources + +- **In-client experience**: https://developers.zoom.us/docs/zoom-apps/guides/in-client-experience/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/breakout-rooms.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/breakout-rooms.md new file mode 100644 index 00000000..4657d93c --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/breakout-rooms.md @@ -0,0 +1,113 @@ +# Breakout Room Integration + +Detect breakout rooms, track room changes, and sync state across rooms. + +## Overview + +When a meeting uses breakout rooms, each room gets its own meeting UUID. Your app needs to handle room transitions and optionally sync state between rooms. + +## Key Concepts + +| Property | Description | +|----------|-------------| +| `meetingUUID` | Unique per room (changes when moving to breakout) | +| `parentUUID` | Original meeting UUID (available in breakout rooms) | +| `onBreakoutRoomChange` | Event fired when user moves between rooms | + +## Detecting Room Changes + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'getMeetingUUID', 'getMeetingContext', + 'onBreakoutRoomChange' + ], + version: '0.16' +}); + +// Get current room info +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +console.log('Current room UUID:', meetingUUID); + +// Listen for room changes +zoomSdk.addEventListener('onBreakoutRoomChange', async (event) => { + console.log('Room changed:', event); + + // Get new room UUID + const { meetingUUID: newUUID } = await zoomSdk.getMeetingUUID(); + console.log('Now in room:', newUUID); + + // Re-initialize room-specific state + await loadRoomState(newUUID); +}); +``` + +## Cross-Room State Sync + +Use your backend to sync state across rooms. The `parentUUID` links all breakout rooms to the main meeting. + +```javascript +// Frontend: Register with backend on room entry +async function registerRoom() { + const { meetingUUID } = await zoomSdk.getMeetingUUID(); + const context = await zoomSdk.getMeetingContext(); + + await fetch('/api/room/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + roomUUID: meetingUUID, + parentUUID: context.parentUUID || meetingUUID, + // parentUUID is null in main room, set in breakout rooms + }) + }); +} + +// Backend: Aggregate state across all rooms +app.get('/api/meeting/:parentUUID/state', async (req, res) => { + const rooms = await db.find({ parentUUID: req.params.parentUUID }); + const aggregated = rooms.reduce((acc, room) => { + // Merge state from all rooms + return { ...acc, ...room.state }; + }, {}); + res.json(aggregated); +}); +``` + +## Managing Breakout Rooms via REST API + +Create and manage breakout rooms from your backend using the Zoom REST API: + +```javascript +// Create breakout rooms (requires meeting:write scope) +const response = await axios.post( + `https://api.zoom.us/v2/meetings/${meetingId}/batch_registrants`, + { + rooms: [ + { name: 'Room 1', participants: ['user1@example.com'] }, + { name: 'Room 2', participants: ['user2@example.com'] } + ] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` } } +); +``` + +## Pattern: Room-Specific vs Global State + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +const context = await zoomSdk.getMeetingContext(); +const parentUUID = context.parentUUID || meetingUUID; + +// Room-specific state (e.g., room discussion notes) +const roomState = await loadState(`room:${meetingUUID}`); + +// Global state (e.g., meeting agenda visible in all rooms) +const globalState = await loadState(`meeting:${parentUUID}`); +``` + +## Resources + +- **Breakout rooms docs**: https://developers.zoom.us/docs/zoom-apps/guides/breakout-rooms/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/collaborate-mode.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/collaborate-mode.md new file mode 100644 index 00000000..4f9874e1 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/collaborate-mode.md @@ -0,0 +1,171 @@ +# Collaborate Mode + +Real-time shared app state across all meeting participants. + +## Overview + +Collaborate mode lets participants share an app experience simultaneously - like Google Docs for Zoom Apps. When one user makes a change, all participants see it in real time. + +## Starting Collaborate Mode + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'startCollaborate', 'onCollaborateChange', + 'connect', 'postMessage', 'onConnect', 'onMessage', + 'getMeetingUUID' + ], + version: '0.16' +}); + +// Host starts collaborate mode +await zoomSdk.startCollaborate({ + shareScreen: true // Also share the app screen +}); + +// Listen for collaborate state changes +zoomSdk.addEventListener('onCollaborateChange', (event) => { + console.log('Collaborate state changed:', event); +}); +``` + +## State Synchronization Patterns + +### Pattern 1: Server Relay (Socket.io) + +Best for apps with a backend. Server is the source of truth. + +```javascript +// Frontend +const socket = io('https://your-server.com'); +const meetingUUID = await zoomSdk.getMeetingUUID(); + +socket.emit('join-room', { roomId: meetingUUID.meetingUUID }); + +// Send state changes +function updateState(key, value) { + socket.emit('state-update', { key, value }); +} + +// Receive state changes +socket.on('state-update', ({ key, value }) => { + applyStateChange(key, value); +}); +``` + +### Pattern 2: SDK Message Passing + +No server needed. Direct peer messaging via `connect()` + `postMessage()`. + +```javascript +// All participants connect +await zoomSdk.connect(); + +zoomSdk.addEventListener('onConnect', () => { + console.log('Connected to other instances'); +}); + +// Send state to all connected instances +async function broadcastState(state) { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'state-sync', data: state }) + }); +} + +// Receive state from other instances +zoomSdk.addEventListener('onMessage', (event) => { + const message = JSON.parse(event.payload); + if (message.type === 'state-sync') { + applyState(message.data); + } +}); +``` + +### Pattern 3: CRDT (Y.js) + +Best for collaborative editing (text, whiteboards). Conflict-free resolution. + +```javascript +import * as Y from 'yjs'; +import { WebrtcProvider } from 'y-webrtc'; + +// Use meeting UUID as room name +const meetingUUID = await zoomSdk.getMeetingUUID(); + +const ydoc = new Y.Doc(); +const provider = new WebrtcProvider(meetingUUID.meetingUUID, ydoc, { + signaling: ['wss://your-signaling-server.com'] +}); + +// Shared state automatically syncs via CRDT +const sharedMap = ydoc.getMap('app-state'); + +// Update state (syncs to all peers automatically) +sharedMap.set('counter', (sharedMap.get('counter') || 0) + 1); + +// Observe changes +sharedMap.observe((event) => { + event.keysChanged.forEach((key) => { + console.log(`${key} changed to:`, sharedMap.get(key)); + }); +}); +``` + +**Note:** The texteditor sample app uses public Y.js signaling servers. For production, host your own signaling server. + +## Example: Shared Counter + +```javascript +import zoomSdk from '@zoom/appssdk'; + +let count = 0; + +async function init() { + await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage'], + version: '0.16' + }); + + await zoomSdk.connect(); + + zoomSdk.addEventListener('onMessage', (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'count-update') { + count = msg.count; + render(); + } + }); + + render(); +} + +async function increment() { + count++; + render(); + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'count-update', count }) + }); +} + +function render() { + document.getElementById('counter').textContent = count; +} +``` + +## Meeting UUID as Room Identifier + +Use `getMeetingUUID()` to get a unique room ID for state synchronization: + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +// Use as Socket.io room, Y.js document name, Redis key, etc. +``` + +This UUID is unique per meeting instance and consistent across all participants. + +## Resources + +- **Collaborate docs**: https://developers.zoom.us/docs/zoom-apps/guides/collaborate-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-texteditor-vuejs diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/guest-mode.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/guest-mode.md new file mode 100644 index 00000000..1f19a13e --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/guest-mode.md @@ -0,0 +1,121 @@ +# Guest Mode + +Handle unauthenticated meeting participants who haven't authorized your app. + +## Overview + +When a meeting has external guests (non-Zoom users or users who haven't installed your app), they enter your app in an unauthenticated state. Guest mode lets you progressively request authorization. + +## Three Authorization States + +``` +Unauthenticated ──> Authenticated ──> Authorized + │ │ │ + │ │ │ + No Zoom identity Has Zoom identity Has OAuth tokens + External guest Zoom user, no app Full app access + Limited UI Can promptAuthorize All APIs available +``` + +| State | Who | getUserContext Returns | Can Use APIs | +|-------|-----|----------------------|-------------| +| **Unauthenticated** | External guests, no Zoom account | Minimal (no user ID) | Very limited | +| **Authenticated** | Zoom users who haven't authorized your app | Name, role (no personal data) | Read-only context | +| **Authorized** | Users who authorized your app | Full user data | All APIs | + +## Detecting State + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['getUserContext', 'authorize', 'promptAuthorize', 'onAuthorized'], + version: '0.16' +}); + +const user = await zoomSdk.getUserContext(); + +if (user.status === 'authorized') { + showFullApp(); +} else if (user.status === 'authenticated') { + showLimitedApp(); + showAuthorizeButton(); +} else { + showGuestView(); + showAuthorizeButton(); +} +``` + +## Prompting Authorization + +```javascript +// Show a button that triggers authorization +document.getElementById('authorize-btn').addEventListener('click', async () => { + try { + await zoomSdk.promptAuthorize(); + } catch (e) { + console.error('Authorization prompt failed:', e); + } +}); + +// Listen for successful authorization +zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + + // Exchange code for tokens on your backend + await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, state }) + }); + + // Upgrade UI to full access + showFullApp(); +}); +``` + +## UI Pattern + +```javascript +function showGuestView() { + document.getElementById('app').innerHTML = ` +

Welcome, Guest!

+

You're viewing this app as a guest. Authorize to unlock all features.

+
+ +
+ + `; +} + +function showLimitedApp() { + document.getElementById('app').innerHTML = ` +

Welcome!

+

Authorize to unlock all features.

+
+ +
+ + `; +} + +function showFullApp() { + document.getElementById('app').innerHTML = ` +

Full App Access

+
+ +
+ `; +} +``` + +## Host-Required Authorization + +Meeting hosts can require all participants to authorize before using the app. This is configured in the Marketplace app settings under the "Guest Mode" feature. + +When enabled, unauthenticated/authenticated users see the authorization prompt immediately. + +## Resources + +- **Guest mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/guest-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-advancedsample-react (includes guest mode) diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/in-client-oauth.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/in-client-oauth.md new file mode 100644 index 00000000..af135c28 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/in-client-oauth.md @@ -0,0 +1,222 @@ +# In-Client OAuth with PKCE + +Authorize users without leaving the Zoom client. Best UX for returning users. + +## Why In-Client OAuth? + +| Approach | UX | When to Use | +|----------|----|-------------| +| **Web redirect** | Opens browser, redirects back | Initial install from Marketplace | +| **In-Client** | Popup inside Zoom, no redirect | Subsequent authorizations, best UX | + +## Flow + +``` +Frontend Backend Zoom +──────── ──────── ──── +GET /api/auth/challenge --> + <-- { codeChallenge, state } + (stores code_verifier in session) + +zoomSdk.authorize({ --> --> Shows "Authorize" popup + codeChallenge, state to user +}) + +onAuthorized fires <-- <-- User clicks "Allow" +{ code, state } + +POST /api/auth/token --> +{ code, state } Validates state + Exchanges code + verifier + for access_token + <-- { success: true } +``` + +## Frontend Implementation + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + await zoomSdk.config({ + capabilities: ['authorize', 'onAuthorized', 'getUserContext'], + version: '0.16' + }); + + // Check if already authorized + try { + const response = await fetch('/api/auth/status'); + const { authorized } = await response.json(); + if (authorized) { + showApp(); + return; + } + } catch (e) { + // Not authorized yet + } + + // Set up authorization listener BEFORE calling authorize + zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + + const response = await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, state }) + }); + + if (response.ok) { + showApp(); + } else { + showError('Authorization failed'); + } + }); + + // Get challenge and start authorization + const challengeResponse = await fetch('/api/auth/challenge'); + const { codeChallenge, state } = await challengeResponse.json(); + + await zoomSdk.authorize({ codeChallenge, state }); +} +``` + +## Backend Implementation + +```javascript +const crypto = require('crypto'); +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +// Generate PKCE challenge +router.get('/api/auth/challenge', (req, res) => { + const verifier = crypto.randomBytes(32).toString('hex'); + const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + const state = crypto.randomBytes(16).toString('hex'); + + // Store in session (server-side only) + req.session.codeVerifier = verifier; + req.session.state = state; + + res.json({ codeChallenge: challenge, state }); +}); + +// Exchange authorization code for tokens +router.post('/api/auth/token', async (req, res) => { + const { code, state } = req.body; + + // Validate state (CSRF protection) + if (state !== req.session.state) { + return res.status(403).json({ error: 'Invalid state' }); + } + + try { + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + // Store tokens securely (session, Redis, or database) + req.session.tokens = { + access_token: tokenResponse.data.access_token, + refresh_token: tokenResponse.data.refresh_token, + expires_at: Date.now() + (tokenResponse.data.expires_in * 1000) + }; + + // Clean up PKCE data + delete req.session.codeVerifier; + delete req.session.state; + + res.json({ success: true }); + } catch (error) { + console.error('Token exchange failed:', error.response?.data || error.message); + res.status(500).json({ error: 'Token exchange failed' }); + } +}); + +// Check authorization status +router.get('/api/auth/status', (req, res) => { + const tokens = req.session.tokens; + const authorized = tokens && tokens.expires_at > Date.now(); + res.json({ authorized }); +}); + +// Token refresh helper +async function refreshTokens(req) { + const { refresh_token } = req.session.tokens; + + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'refresh_token', + refresh_token + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + req.session.tokens = { + access_token: response.data.access_token, + refresh_token: response.data.refresh_token, + expires_at: Date.now() + (response.data.expires_in * 1000) + }; + + return req.session.tokens; +} + +module.exports = router; +``` + +## Re-Authorization with promptAuthorize + +For guest mode users who need to upgrade their authorization: + +```javascript +// Use when user needs to grant additional permissions +await zoomSdk.promptAuthorize(); + +// Same onAuthorized listener fires +zoomSdk.addEventListener('onAuthorized', async (event) => { + // Handle same as initial authorization +}); +``` + +## Token Refresh Pattern + +```javascript +// Middleware to auto-refresh expired tokens +async function ensureAuthorized(req, res, next) { + if (!req.session.tokens) { + return res.status(401).json({ error: 'Not authorized' }); + } + + // Refresh if expiring within 5 minutes + if (req.session.tokens.expires_at < Date.now() + 300000) { + try { + await refreshTokens(req); + } catch (error) { + return res.status(401).json({ error: 'Token refresh failed' }); + } + } + + next(); +} +``` + +## Resources + +- **Auth guide**: https://developers.zoom.us/docs/zoom-apps/authentication/ +- **OAuth reference**: [../references/oauth.md](../references/oauth.md) diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-camera.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-camera.md new file mode 100644 index 00000000..7614ed85 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-camera.md @@ -0,0 +1,218 @@ +# Layers API - Camera Mode + +Overlay graphics on the user's own camera feed. Create virtual camera effects, branded frames, and interactive overlays. + +## Overview + +Camera mode overlays your content on the user's camera video. Unlike immersive mode (which controls the entire meeting view), camera mode only affects the individual user's camera feed - other participants see the overlay on that user's video. + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +const config = await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'postMessage', 'onMessage', + 'onRenderedAppOpened' + ], + version: '0.16' +}); + +// renderTarget = virtual camera frame size (default: 1280x720) +const rtWidth = config.media?.renderTarget?.width || 1280; +const rtHeight = config.media?.renderTarget?.height || 720; + +// Start camera mode +await zoomSdk.runRenderingContext({ view: 'camera' }); + +// Wait for CEF to initialize +zoomSdk.addEventListener('onRenderedAppOpened', async () => { + // Draw self video as background + await zoomSdk.drawParticipant({ + participantUUID: myUUID, + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 1, + cameraModeMirroring: true // v5.13.5+ — mirror for self-view + }); + + // Add a branded frame overlay + const frame = await createBrandedFrame(rtWidth, rtHeight); + const imageData = frame.getContext('2d').getImageData(0, 0, rtWidth, rtHeight); + await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 2 + }); + + // Or draw webview overlay (your app's home URL rendered off-screen) + await zoomSdk.drawWebView({ + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 3 + }); +}); +``` + +## CEF Race Condition (Critical) + +Camera mode uses CEF (Chromium Embedded Framework) which takes time to initialize. Drawing too early will fail silently. + +**Solution: Retry with backoff** + +```javascript +async function drawWithRetry(drawFn, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await drawFn(); + return; // Success + } catch (error) { + if (i === maxRetries - 1) throw error; + // Exponential backoff: 200ms, 400ms, 800ms, 1600ms, 3200ms + await new Promise(r => setTimeout(r, 200 * Math.pow(2, i))); + } + } +} + +// Usage +await zoomSdk.runRenderingContext({ view: 'camera' }); + +await drawWithRetry(async () => { + await zoomSdk.drawImage({ + imageData: frame.toDataURL(), + x: 0, y: 0, width: 1280, height: 720, zIndex: 1 + }); +}); +``` + +## Example: Branded Camera Frame + +```javascript +async function createBrandedFrame() { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + + // Transparent center (camera shows through) + ctx.clearRect(0, 0, 1280, 720); + + // Bottom bar with company branding + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 660, 1280, 60); + + // Company name + ctx.fillStyle = 'white'; + ctx.font = 'bold 20px sans-serif'; + ctx.fillText('Acme Corp', 20, 695); + + // Border frame + ctx.strokeStyle = '#2d8cff'; + ctx.lineWidth = 4; + ctx.strokeRect(2, 2, 1276, 716); + + return canvas; +} +``` + +## Example: Name Tag Overlay + +```javascript +async function drawNameTag(name, title) { + const canvas = document.createElement('canvas'); + canvas.width = 300; + canvas.height = 80; + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = 'rgba(45, 140, 255, 0.85)'; + ctx.beginPath(); + ctx.roundRect(0, 0, 300, 80, 12); + ctx.fill(); + + // Name + ctx.fillStyle = 'white'; + ctx.font = 'bold 22px sans-serif'; + ctx.fillText(name, 16, 32); + + // Title + ctx.font = '16px sans-serif'; + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillText(title, 16, 58); + + await zoomSdk.drawImage({ + imageData: canvas.toDataURL(), + x: 20, y: 620, + width: 300, height: 80, + zIndex: 2 + }); +} +``` + +## Exiting Camera Mode + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +## drawWebView in Camera Mode + +The webview renders your app's home URL off-screen. Use it for interactive overlays controlled from your sidebar app: + +```javascript +// Draw webview filling entire camera frame +await zoomSdk.drawWebView({ + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 2 +}); + +// Or partial overlay (bottom third) +await zoomSdk.drawWebView({ + x: 0, y: rtHeight * 0.67, + width: rtWidth, height: rtHeight * 0.33, + zIndex: 2 +}); + +// Hide webview (app keeps running) +await zoomSdk.clearWebView(); +``` + +**Communication between sidebar ↔ camera mode app:** + +```javascript +// Sidebar sends command to camera mode instance +zoomSdk.postMessage({ command: 'show-nametag', name: 'John' }); + +// Camera mode instance listens (no connect() required) +zoomSdk.addEventListener('onMessage', (event) => { + if (event.command === 'show-nametag') { + document.getElementById('name').textContent = event.name; + } +}); +``` + +## Differences from Immersive Mode + +| Aspect | Immersive | Camera | +|--------|-----------|--------| +| Scope | Entire meeting view | User's camera only | +| drawParticipant | Any participant | Self only | +| drawWebView | Yes | Yes | +| Who sees it | All participants | All see it on this user's feed | +| Use case | Custom layouts | Personal overlays, branding | +| Browser | Standard WebView | CEF (has init delay) | +| Coordinate space | CSS pixels | Raw pixels (renderTarget) | +| `cameraModeMirroring` | N/A | Yes (v5.13.5+) | + +## Resources + +- **Camera mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/camera-mode/ +- **Layers API reference**: [../references/layers-api.md](../references/layers-api.md) +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-immersive.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-immersive.md new file mode 100644 index 00000000..93e141a6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/layers-immersive.md @@ -0,0 +1,285 @@ +# Layers API - Immersive Mode + +Custom video layouts that replace the standard gallery view. Position participant video feeds, backgrounds, and web content anywhere on screen. + +## Overview + +Immersive mode takes over the entire meeting video area. You control where each participant's video appears, add background images, and overlay web content. + +**Use cases:** Podcast layout, talk show, classroom, game show, branded meetings. + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// 1. Config with Layers capabilities +await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'getMeetingParticipants', 'onParticipantChange', + 'postMessage', 'onMessage', + 'sendAppInvitationToAllParticipants', + 'onRenderedAppOpened' + ], + version: '0.16' +}); + +// 2. Start immersive mode (Team = person cutout, Presentation = rectangle) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'person' // Removes backgrounds via AI segmentation +}); + +// 3. Draw a background (imageData = JS ImageData object, NOT base64) +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +ctx.fillStyle = '#1a1a2e'; +ctx.fillRect(0, 0, 1280, 720); +const imageData = ctx.getImageData(0, 0, 1280, 720); + +await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); + +// 4. Position participants +const { participants } = await zoomSdk.getMeetingParticipants(); + +await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 50, y: 100, + width: 500, height: 400, + zIndex: 1, + cutout: 'person' // Override default if needed +}); + +await zoomSdk.drawParticipant({ + participantUUID: participants[1].participantUUID, + x: 730, y: 100, + width: 500, height: 400, + zIndex: 1, + cutout: 'person' +}); +``` + +## Drawing Methods + +### drawParticipant + +Position a participant's video feed. In immersive mode, you can draw any participant. + +```javascript +await zoomSdk.drawParticipant({ + participantUUID: 'uuid-string', // From getMeetingParticipants() + x: 0, // PixelValue: "Npx", "N%", or number + y: 0, // PixelValue + width: 640, // PixelValue (aspect ratio maintained) + height: 480, // PixelValue (aspect ratio maintained) + zIndex: 1, // Stacking order (higher = on top) + cutout: 'person' // Optional: "person"|"standard"|"rectangle"|"circle"|"square"|"verticalRectangle" +}); +``` + +**Cutout shapes** (all have 30px rounded corners except `"standard"`): +- `"person"` — AI background removal (v5.9.3+) +- `"standard"` — Full uncropped video, squared corners (v5.11.3+) +- `"rectangle"` — Rounded rectangle (v5.11.0+) +- `"circle"` — Circle (v5.11.3+) +- `"square"` — Square with rounded corners (v5.11.3+) +- `"verticalRectangle"` — Vertical rectangle with rounded corners (v5.11.3+) + +> **Deprecated:** `participantId` — use `participantUUID` instead. + +### drawImage + +Add images (backgrounds, overlays, borders). Uses standard JavaScript `ImageData` (NOT base64): + +```javascript +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +// ... draw on canvas ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const { imageId } = await zoomSdk.drawImage({ + imageData, // ImageData object from canvas.getImageData() + x: 0, y: 0, + zIndex: 0 // Behind participants +}); +// Save imageId for clearImage() later +``` + +### drawWebView + +Embed your app's webview as an interactive overlay. Only one webview per rendering context. + +```javascript +await zoomSdk.drawWebView({ + x: 400, y: 600, + width: 480, height: 100, + zIndex: 2 // On top of everything +}); +``` + +> See [../references/layers-api.md](../references/layers-api.md#drawwebview) for full drawWebView details, webview communication, and the `webviewId` documentation inconsistency. + +### Clearing + +```javascript +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); +await zoomSdk.clearImage({ imageId: 'id-from-drawImage-response' }); +await zoomSdk.clearWebView(); // No params per TypeDoc v0.16.36 +``` + +### Exit Immersive Mode + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +## Complete Example: Podcast Layout + +Two hosts side-by-side with custom background: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +class PodcastLayout { + constructor() { + this.active = false; + } + + async start() { + await zoomSdk.runRenderingContext({ view: 'immersive', defaultCutout: 'person' }); + this.active = true; + + // Draw background + await this.drawBackground(); + + // Position hosts + const { participants } = await zoomSdk.getMeetingParticipants(); + await this.layoutParticipants(participants); + + // React to participant changes + zoomSdk.addEventListener('onParticipantChange', async () => { + const { participants } = await zoomSdk.getMeetingParticipants(); + await this.layoutParticipants(participants); + }); + } + + async drawBackground() { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + + // Gradient background + const gradient = ctx.createLinearGradient(0, 0, 1280, 720); + gradient.addColorStop(0, '#1a1a2e'); + gradient.addColorStop(1, '#16213e'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 1280, 720); + + // Title + ctx.fillStyle = 'white'; + ctx.font = 'bold 32px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('The Zoom Podcast', 640, 60); + + const imageData = ctx.getImageData(0, 0, 1280, 720); + await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, zIndex: 0 + }); + } + + async layoutParticipants(participants) { + if (participants.length === 1) { + // Single host - centered + await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 340, y: 100, width: 600, height: 500, zIndex: 1 + }); + } else if (participants.length >= 2) { + // Two hosts - side by side + await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 40, y: 100, width: 580, height: 500, zIndex: 1 + }); + await zoomSdk.drawParticipant({ + participantUUID: participants[1].participantUUID, + x: 660, y: 100, width: 580, height: 500, zIndex: 1 + }); + } + } + + async stop() { + await zoomSdk.closeRenderingContext(); + this.active = false; + } +} +``` + +## HiDPI Support + +For Retina/HiDPI displays, multiply coordinates by `window.devicePixelRatio`: + +```javascript +const dpr = window.devicePixelRatio || 1; + +await zoomSdk.drawParticipant({ + participantUUID: uuid, + x: 100 * dpr, + y: 100 * dpr, + width: 640 * dpr, + height: 480 * dpr, + zIndex: 1 +}); +``` + +## Multi-Participant Sync + +The host controls the layout. Use Socket.io to broadcast layout changes: + +```javascript +// Host sends layout to all participants via your backend +socket.emit('layout-change', { + participants: [ + { uuid: 'a', x: 40, y: 100, w: 580, h: 500 }, + { uuid: 'b', x: 660, y: 100, w: 580, h: 500 } + ] +}); + +// All participants apply the layout +socket.on('layout-change', async (layout) => { + for (const p of layout.participants) { + await zoomSdk.drawParticipant({ + participantUUID: p.uuid, + x: p.x, y: p.y, width: p.w, height: p.h, zIndex: 1 + }); + } +}); +``` + +## Performance Tips + +- Use `requestAnimationFrame` for animations +- Minimize `drawImage` calls (batch updates) +- Pre-render complex backgrounds to canvas +- Keep zIndex values low (0-10 range) +- Clear unused elements to free resources + +## Resources + +- **Layers docs**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Layers API reference**: [../references/layers-api.md](../references/layers-api.md) +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/quick-start.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/quick-start.md new file mode 100644 index 00000000..92c49bf4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/examples/quick-start.md @@ -0,0 +1,258 @@ +# Quick Start - Hello World Zoom App + +Complete working Zoom App with Express backend and SDK frontend. + +## Prerequisites + +- Node.js 18+ +- ngrok account (free tier works) +- Zoom Marketplace app (type: "Zoom App") + +## Project Setup + +### package.json + +```json +{ + "name": "zoom-app-hello-world", + "version": "1.0.0", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.0", + "cookie-session": "^2.0.0", + "axios": "^1.6.0", + "dotenv": "^16.0.0" + } +} +``` + +### .env + +```ini +ZOOM_APP_CLIENT_ID=your_client_id +ZOOM_APP_CLIENT_SECRET=your_client_secret +ZOOM_APP_REDIRECT_URI=https://xxxxx.ngrok.io/auth +SESSION_SECRET=generate_a_random_string_here +``` + +### server.js + +```javascript +require('dotenv').config(); +const express = require('express'); +const crypto = require('crypto'); +const cookieSession = require('cookie-session'); +const axios = require('axios'); + +const app = express(); +app.use(express.json()); +app.use(express.static('public')); + +// Cookie session - SameSite=None required for Zoom embedded browser +app.use(cookieSession({ + name: 'session', + keys: [process.env.SESSION_SECRET], + maxAge: 24 * 60 * 60 * 1000, + sameSite: 'none', + secure: true +})); + +// OWASP security headers (required for Marketplace approval) +app.use((req, res, next) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Content-Security-Policy', "frame-ancestors 'self' zoom.us *.zoom.us"); + res.setHeader('Referrer-Policy', 'same-origin'); + next(); +}); + +// Step 1: Install - redirect to Zoom OAuth +app.get('/install', (req, res) => { + const verifier = crypto.randomBytes(32).toString('hex'); + const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); + const state = crypto.randomBytes(16).toString('hex'); + + req.session.codeVerifier = verifier; + req.session.state = state; + + const url = `https://zoom.us/oauth/authorize?` + + `client_id=${process.env.ZOOM_APP_CLIENT_ID}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(process.env.ZOOM_APP_REDIRECT_URI)}` + + `&code_challenge=${challenge}` + + `&code_challenge_method=S256` + + `&state=${state}`; + + res.redirect(url); +}); + +// Step 2: OAuth callback - exchange code for tokens +app.get('/auth', async (req, res) => { + const { code, state } = req.query; + + if (state !== req.session.state) { + return res.status(403).send('Invalid state'); + } + + try { + // Exchange authorization code for tokens + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + req.session.tokens = tokenResponse.data; + + // Get deeplink to open app in Zoom client + const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${tokenResponse.data.access_token}` } } + ); + + res.redirect(deeplink.data.deeplink); + } catch (error) { + console.error('OAuth error:', error.response?.data || error.message); + res.status(500).send('Authorization failed'); + } +}); + +// API endpoint for frontend +app.get('/api/user', (req, res) => { + if (!req.session.tokens) { + return res.status(401).json({ error: 'Not authenticated' }); + } + res.json({ authenticated: true }); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); +``` + +### public/index.html + +```html + + + + My Zoom App + + + + +

Hello Zoom App!

+
Loading...
+ + + + +``` + +## Running Locally + +```bash +# 1. Install dependencies +npm install + +# 2. Start ngrok tunnel +ngrok http 3000 + +# 3. Copy the https URL (e.g., https://abc123.ngrok.io) + +# 4. Update .env with ngrok URL +# ZOOM_APP_REDIRECT_URI=https://abc123.ngrok.io/auth + +# 5. Start server +npm run dev +``` + +## Marketplace Configuration + +In [Zoom Marketplace](https://marketplace.zoom.us/) -> Your App: + +1. **App Credentials**: Copy Client ID and Secret to `.env` +2. **Feature tab** -> Zoom App: + - **Home URL**: `https://abc123.ngrok.io` + - **Redirect URL**: `https://abc123.ngrok.io/auth` + - **Domain Allow List**: `abc123.ngrok.io` +3. **Scopes tab**: Add `zoomapp:inmeeting` +4. **Local Test**: Click your app name in Zoom client sidebar to open + +## Next Steps + +- Add [In-Client OAuth](in-client-oauth.md) for seamless re-authorization +- Add [Layers API](layers-immersive.md) for immersive experiences +- Review [Security](../concepts/security.md) headers for Marketplace approval diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/apis.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/apis.md new file mode 100644 index 00000000..14437ccb --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/apis.md @@ -0,0 +1,274 @@ +# Zoom Apps SDK - API Reference + +Complete reference for all @zoom/appssdk methods organized by category. + +## Initialization + +### config(options) + +**Must be called first.** Initializes the SDK and declares capabilities. + +```javascript +const configResponse = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'shareApp', ...], + version: '0.16' +}); +// Returns: { runningContext, clientVersion, unsupportedApis } +``` + +### getSupportedJsApis() + +Check which APIs are available in the current client. + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); +// Returns: { supportedApis: ['getMeetingContext', 'shareApp', ...] } +``` + +## Context APIs + +### getMeetingContext() + +```javascript +const context = await zoomSdk.getMeetingContext(); +// { meetingID, meetingTopic, meetingStatus } +``` + +### getUserContext() + +```javascript +const user = await zoomSdk.getUserContext(); +// { screenName, role, participantId, status } +// role: 'host' | 'coHost' | 'attendee' | 'panelist' +// status: 'authorized' | 'authenticated' | 'unauthenticated' +``` + +### getRunningContext() + +```javascript +const { context } = await zoomSdk.getRunningContext(); +// 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... +``` + +### getMeetingUUID() + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +``` + +### getMeetingJoinUrl() + +```javascript +const { joinUrl } = await zoomSdk.getMeetingJoinUrl(); +``` + +### getMeetingParticipants() + +```javascript +const { participants } = await zoomSdk.getMeetingParticipants(); +// [{ participantId, participantUUID, screenName, role }] +``` + +### getRecordingContext() + +```javascript +const recording = await zoomSdk.getRecordingContext(); +// { cloudRecordingStatus, localRecordingStatus } +``` + +## Authorization APIs + +### authorize(options) + +Trigger In-Client OAuth (no browser redirect). + +```javascript +await zoomSdk.authorize({ codeChallenge: '...', state: '...' }); +// onAuthorized event fires with { code, state } +``` + +### promptAuthorize() + +Prompt guest/authenticated users to authorize. + +```javascript +await zoomSdk.promptAuthorize(); +``` + +## Meeting Action APIs + +### shareApp() + +```javascript +await zoomSdk.shareApp(); +``` + +### sendAppInvitation(options) + +```javascript +await zoomSdk.sendAppInvitation({ participantUUIDs: ['uuid1'], action: 'open' }); +``` + +### sendAppInvitationToAllParticipants() + +```javascript +await zoomSdk.sendAppInvitationToAllParticipants(); +``` + +### sendAppInvitationToMeetingOwner() + +```javascript +await zoomSdk.sendAppInvitationToMeetingOwner(); +``` + +### showAppInvitationDialog() + +```javascript +await zoomSdk.showAppInvitationDialog(); +``` + +## UI APIs + +### expandApp(options) + +```javascript +await zoomSdk.expandApp({ action: 'expand' }); // or 'collapse' +``` + +### openUrl(options) + +```javascript +await zoomSdk.openUrl({ url: 'https://example.com' }); +``` + +### showNotification(options) + +```javascript +await zoomSdk.showNotification({ title: 'Alert', message: 'Something happened!', type: 'info' }); +``` + +## Media APIs + +### listCameras() + +```javascript +const { cameras } = await zoomSdk.listCameras(); +``` + +### setVirtualBackground(options) + +```javascript +await zoomSdk.setVirtualBackground({ imageData: 'base64-data' }); +``` + +### removeVirtualBackground() + +```javascript +await zoomSdk.removeVirtualBackground(); +``` + +### setVideoMirrorEffect(options) + +```javascript +await zoomSdk.setVideoMirrorEffect({ mirrorMyVideo: true }); +``` + +### allowParticipantToRecord(options) + +```javascript +await zoomSdk.allowParticipantToRecord({ participantUUID: 'uuid', action: 'grant' }); +``` + +### cloudRecording(options) + +```javascript +await zoomSdk.cloudRecording({ action: 'start' }); // 'stop', 'pause', 'resume' +``` + +## Communication APIs + +### connect() + +Connect to other app instances (main client <-> meeting). + +```javascript +await zoomSdk.connect(); +``` + +### postMessage(options) + +```javascript +await zoomSdk.postMessage({ payload: JSON.stringify({ type: 'update', data: {...} }) }); +``` + +## Layers APIs + +### runRenderingContext(options) + +```javascript +await zoomSdk.runRenderingContext({ view: 'immersive' }); // or 'camera' +``` + +### closeRenderingContext() + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +### drawParticipant(options) + +```javascript +await zoomSdk.drawParticipant({ participantUUID: 'uuid', x: 0, y: 0, width: 640, height: 480, zIndex: 1 }); +``` + +### clearParticipant(options) + +```javascript +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); +``` + +### drawImage(options) + +```javascript +await zoomSdk.drawImage({ imageData: canvas.toDataURL(), x: 0, y: 0, width: 1280, height: 720, zIndex: 0 }); +``` + +### clearImage(options) + +```javascript +await zoomSdk.clearImage({ imageId: 'id' }); +``` + +### drawWebView(options) + +```javascript +await zoomSdk.drawWebView({ webviewId: 'my-view', x: 0, y: 0, width: 400, height: 300, zIndex: 2 }); +``` + +### clearWebView(options) + +```javascript +await zoomSdk.clearWebView({ webviewId: 'my-view' }); +``` + +## Collaborate APIs + +### startCollaborate(options) + +```javascript +await zoomSdk.startCollaborate({ shareScreen: true }); +``` + +## Event Listeners + +```javascript +zoomSdk.addEventListener('onMeeting', (event) => { ... }); +zoomSdk.removeEventListener('onMeeting', handler); +``` + +See **[events.md](events.md)** for complete event reference. + +## Resources + +- **SDK Reference**: https://appssdk.zoom.us/ +- **Capabilities list**: https://developers.zoom.us/docs/zoom-apps/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/environment-variables.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/environment-variables.md new file mode 100644 index 00000000..4d655e04 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/environment-variables.md @@ -0,0 +1,24 @@ +# Zoom Apps SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_APP_CLIENT_ID` | Yes | OAuth and app identity | Zoom Marketplace -> your Zoom App -> App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> your Zoom App -> App Credentials | +| `ZOOM_APP_REDIRECT_URI` | Yes | OAuth callback URL | Zoom Marketplace -> your Zoom App -> OAuth allow list / redirect settings | +| `ZOOM_APP_URL` | Usually | URL loaded by Zoom client | Zoom Marketplace -> your Zoom App -> Basic Information (Development URL / Home URL) | +| `ZOOM_APP_BASE_URL` | Optional | Internal base URL alias | Set to your deployed app origin | +| `SESSION_SECRET` | Recommended | Session signing/encryption | Generate and manage in your own secret manager | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +Generate these during OAuth flow; do not hardcode them in repo files. + +## Notes + +- Keep `ZOOM_APP_CLIENT_SECRET` server-side only. +- Use separate development and production app credentials. diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/events.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/events.md new file mode 100644 index 00000000..5f8a4a30 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/events.md @@ -0,0 +1,205 @@ +# Zoom Apps SDK - Events Reference + +Subscribe to events to respond to meeting, user, and app state changes. + +**IMPORTANT**: Register event listeners AFTER successful `zoomSdk.config()` call. Events must be listed in the `capabilities` array. + +## Subscribing to Events + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['onMeeting', 'onParticipantChange', 'onAuthorized', ...], + version: '0.16' +}); + +zoomSdk.addEventListener('onMeeting', (event) => { + console.log('Meeting event:', event); +}); +``` + +## Meeting Events + +### onMeeting + +Meeting lifecycle changes (start, end, etc.). + +```javascript +zoomSdk.addEventListener('onMeeting', (event) => { + // event: { action: 'started' | 'ended' | ... } +}); +``` + +### onParticipantChange + +Participants join or leave. + +```javascript +zoomSdk.addEventListener('onParticipantChange', (event) => { + const { participants, action } = event; + // action: 'join' | 'leave' + // participants: [{ participantId, screenName, role }] +}); +``` + +### onActiveSpeakerChange + +Active speaker changes. + +```javascript +zoomSdk.addEventListener('onActiveSpeakerChange', (event) => { + const { participantId, screenName } = event; +}); +``` + +### onBreakoutRoomChange + +User moves between breakout rooms. + +```javascript +zoomSdk.addEventListener('onBreakoutRoomChange', (event) => { + // Re-fetch meeting UUID and state for new room +}); +``` + +## User Events + +### onMyUserContextChange + +Current user's context changes (role, name, mute status, etc.). + +```javascript +zoomSdk.addEventListener('onMyUserContextChange', (event) => { + const { role, screenName } = event; +}); +``` + +### onMyMediaChange + +Current user's audio/video status changes. + +```javascript +zoomSdk.addEventListener('onMyMediaChange', (event) => { + const { audio, video } = event; + // audio: { muted: true/false } + // video: { started: true/false } +}); +``` + +## App Events + +### onShareApp + +App sharing status changes. + +```javascript +zoomSdk.addEventListener('onShareApp', (event) => { + const { isShared } = event; +}); +``` + +### onAppPopout + +App popped out to separate window or back. + +```javascript +zoomSdk.addEventListener('onAppPopout', (event) => { + const { isPopout } = event; +}); +``` + +### onRunningContextChange + +Running context changes (e.g., meeting starts while in main client). + +```javascript +zoomSdk.addEventListener('onRunningContextChange', (event) => { + const { runningContext } = event; +}); +``` + +## Authorization Events + +### onAuthorized + +In-Client OAuth authorization completed. + +```javascript +zoomSdk.addEventListener('onAuthorized', (event) => { + const { code, state } = event; + // Send code to backend for token exchange +}); +``` + +## Communication Events + +### onConnect + +Another app instance connected. + +```javascript +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Other instance connected'); +}); +``` + +### onMessage + +Message received from connected instance. + +```javascript +zoomSdk.addEventListener('onMessage', (event) => { + const data = JSON.parse(event.payload); +}); +``` + +## Collaborate Events + +### onCollaborateChange + +Collaborate mode state changes. + +```javascript +zoomSdk.addEventListener('onCollaborateChange', (event) => { + console.log('Collaborate state:', event); +}); +``` + +## Layers Events + +### onRenderedAppOpened + +Immersive/camera rendering context opened. + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', (event) => { + console.log('Rendering context ready'); +}); +``` + +## Reaction Events + +### onReaction + +Participant sends a reaction. + +```javascript +zoomSdk.addEventListener('onReaction', (event) => { + const { participantId, reaction } = event; +}); +``` + +## Removing Listeners + +```javascript +const handler = (event) => { ... }; +zoomSdk.addEventListener('onMeeting', handler); + +// Later, remove it +zoomSdk.removeEventListener('onMeeting', handler); +``` + +## Resources + +- **Events reference**: https://appssdk.zoom.us/ diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/layers-api.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/layers-api.md new file mode 100644 index 00000000..2dc90baa --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/layers-api.md @@ -0,0 +1,501 @@ +# Zoom Apps SDK - Layers API Reference + +Build immersive video layouts and camera overlays using the Layers API. + +## Overview + +The Layers API (v1.5) provides rendering modes for custom visual experiences. Requires Zoom Client v5.10.6+. + +| Mode | Description | Use Case | +|------|-------------|----------| +| **Team** (`immersive` + `person` cutout) | Canvas with background-removed participant cutouts | Podcast, talk show, classroom | +| **Presentation** (`immersive` + `rectangle` cutout) | Canvas with full-width participant video tiles | Presentations, branded meetings | +| **Camera** | Overlay on user's own camera feed (OSR) | Branding, name tags, effects | +| **Controller** | Sidebar app that coordinates Layers modes | Required for all modes above | + +> **Note:** When using the Layers API, your app is categorized as an "Immersive App" on the Marketplace. + +## Required Capabilities + +```javascript +await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'postMessage', 'onMessage', + 'sendAppInvitationToAllParticipants', + 'onMyMediaChange', + 'onRenderedAppOpened' + ], + version: '0.16' +}); +``` + +> **Gotcha:** The official guide lists `clearWebview` (lowercase 'v') in one config example. Use `clearWebView` (camelCase) to match the actual method name. + +## Types + +### PixelValue + +All position/size parameters accept three formats: + +```typescript +type PixelValue = `${string}px` | `${string}%` | number; +``` + +| Format | Example | Meaning | +|--------|---------|---------| +| `"Npx"` | `"100px"` | CSS reference pixels | +| `"N%"` | `"50%"` | Percentage of container/view | +| `number` | `1280` | Raw physical pixels | + +### ParticipantCutoutShape + +```typescript +type ParticipantCutoutShape = + | "person" // v5.9.3+ — Cut out background (AI segmentation) + | "standard" // v5.11.3+ — Full uncropped video (squared corners) + | "rectangle" // v5.11.0+ — Rounded rectangle (30px radius) + | "circle" // v5.11.3+ — Circle + | "square" // v5.11.3+ — Square (30px radius) + | "verticalRectangle" // v5.11.3+ — Vertical rectangle (30px radius) +``` + +All shapes have 30px rounded corners except `"standard"` which has squared corners. + +### RenderingContextView + +```typescript +type RenderingContextView = "immersive" | "camera"; +``` + +## Lifecycle + +### Starting a Rendering Context + +```javascript +// Team mode (person cutout — removes backgrounds) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'person' +}); + +// Presentation mode (rectangle cutout — keeps backgrounds) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'rectangle' +}); + +// Camera mode (affects only your video stream) +await zoomSdk.runRenderingContext({ view: 'camera' }); +``` + +**`runRenderingContext(options)`:** +- `view` (required): `"immersive"` | `"camera"` +- `defaultCutout` (optional): Sets the default cutout shape for all `drawParticipant()` calls in this context + +### Running Context Values + +| Context | Meaning | +|---------|---------| +| `inMeeting` | Default sidebar panel | +| `inImmersive` | Running in immersive mode (team or presentation) | +| `inCamera` | Running as virtual camera (off-screen rendering) | + +```javascript +const { runningContext } = await zoomSdk.getRunningContext(); +// runningContext changes automatically when runRenderingContext() is called +``` + +### Updating Content + +To move, resize, or adjust a drawn element: clear it first, then redraw. + +```javascript +// Move a participant +await zoomSdk.clearParticipant({ participantUUID: uuid }); +await zoomSdk.drawParticipant({ participantUUID: uuid, x: 100, y: 200, width: 640, height: 480, zIndex: 1 }); +``` + +> There is no in-place update — always clear + redraw. + +### Closing + +```javascript +await zoomSdk.closeRenderingContext(); +// Returns app to sidebar, runningContext becomes "inMeeting" +``` + +### Constraints + +- Only a **meeting host** can set the rendering context to immersive +- Only **one immersive context** can exist at a time (second attempt fails with error) +- **Camera mode + Presentation mode can run simultaneously** +- Host must use `sendAppInvitationToAllParticipants` to transition other participants +- If `aomhost` package needs download, `runRenderingContext` returns non-success + +## Drawing Methods + +### drawParticipant + +Position a participant's video feed on the canvas. + +```typescript +drawParticipant(options: DrawParticipantOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `participantUUID` | `string` | — | Meeting-specific participant identifier | +| ~~`participantId`~~ | ~~`string`~~ | — | **DEPRECATED** — use `participantUUID` | +| `x` | `PixelValue` | `"0px"` | Horizontal position | +| `y` | `PixelValue` | `"0px"` | Vertical position | +| `width` | `PixelValue` | `"100%"` | Width (aspect ratio maintained) | +| `height` | `PixelValue` | `"100%"` | Height (aspect ratio maintained) | +| `zIndex` | `number` | `1` | Stacking order (higher = on top) | +| `cutout` | `ParticipantCutoutShape` | context default | Cutout behavior (v5.9.3+) | +| `cameraModeMirroring` | `boolean` | `false` | Mirror video in camera mode (v5.13.5+) | + +**Mode differences:** +- **Immersive:** Can draw any participant +- **Camera:** Can only draw current user (self) + +```javascript +// Immersive — draw any participant with person cutout +await zoomSdk.drawParticipant({ + participantUUID: 'uuid-from-getMeetingParticipants', + x: 40, y: 100, + width: 580, height: 500, + zIndex: 1, + cutout: 'person' +}); + +// Camera — draw self with mirroring +await zoomSdk.drawParticipant({ + participantUUID: myUUID, + x: 0, y: 0, + width: 1280, height: 720, + zIndex: 1, + cameraModeMirroring: true // v5.13.5+ +}); +``` + +### drawImage + +Draw static images (backgrounds, overlays, borders). + +```typescript +drawImage(options: DrawImageOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `imageData` | `ImageData` | — | **Required.** Standard JS ImageData object (width, height, pixel bytes) | +| `x` | `PixelValue` | `"0px"` | Horizontal position | +| `y` | `PixelValue` | `"0px"` | Vertical position | +| `zIndex` | `number` | `1` | Stacking order | + +**Returns:** `{ imageId: string }` — use this ID with `clearImage()`. + +> **Important:** `imageData` is a standard JavaScript `ImageData` object (from `canvas.getImageData()`), NOT a base64 data URL. + +```javascript +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +// ... draw on canvas ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const { imageId } = await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); +``` + +#### HiDPI Constraints + +`drawImage()` does **not** directly support HiDPI image sizes. For HiDPI/Retina: + +1. Draw to canvas using the scaling ratio (`window.devicePixelRatio`) +2. Divide out the ratio for x/y coordinates when passing to `drawImage` +3. Keep the ratio for width and height +4. You may need to **tile** the screen for full-screen images + +```javascript +const dpr = window.devicePixelRatio || 1; +const canvas = document.createElement('canvas'); +canvas.width = 1280 * dpr; +canvas.height = 720 * dpr; +const ctx = canvas.getContext('2d'); +ctx.scale(dpr, dpr); +// ... draw at logical pixels ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); +``` + +### drawWebView + +Position the app's OSR (Off-Screen Rendering) webview within the Layers canvas. + +```typescript +drawWebView(options: DrawWebViewOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `x` | `PixelValue` | `0` | Horizontal position in OSR target area | +| `y` | `PixelValue` | `0` | Vertical position in OSR target area | +| `width` | `PixelValue` | full rendering width | Width in OSR target area | +| `height` | `PixelValue` | full rendering height | Height in OSR target area | +| `zIndex` | `number` | `1` | Stacking order | + +> **⚠ Documentation inconsistency:** The official Zoom guides show a `webviewId` parameter in examples, but the TypeDoc type definition (v0.16.36) does **not** include it. Since there is only one webview per app, this parameter may be vestigial. If in doubt, omit it. + +**What the webview renders:** Your app's home URL as configured in `zoomSdk.config()`. It's an off-screen rendering of your app — not a configurable URL. + +**Only one webview per rendering context.** There is no multi-webview support. + +```javascript +// Full-screen webview in camera mode +const config = await zoomSdk.config({ /* ... */ }); +await zoomSdk.runRenderingContext({ view: 'camera' }); + +await zoomSdk.drawWebView({ + x: 0, + y: 0, + width: config.media.renderTarget.width, // Default: 1280 + height: config.media.renderTarget.height, // Default: 720 + zIndex: 2 +}); +``` + +```javascript +// Partial webview overlay (bottom third of camera) +await zoomSdk.drawWebView({ + x: 0, + y: 480, + width: 1280, + height: 240, + zIndex: 2 +}); +``` + +#### Webview Communication + +The sidebar app and the camera/immersive app are separate instances. Use `postMessage()` and `onMessage` to communicate between them: + +```javascript +// Sidebar instance → Camera instance (no connect() required) +zoomSdk.postMessage({ command: 'update-overlay', text: 'Q&A Time' }); + +// Camera instance listens +zoomSdk.addEventListener('onMessage', (eventInfo) => { + if (eventInfo.command === 'update-overlay') { + document.getElementById('overlay-text').textContent = eventInfo.text; + } +}); +``` + +> **Note:** `connect()` is NOT required for app-to-app messaging in Layers. `postMessage` works between instances of the same app. + +### Clearing + +```javascript +// Clear participant (use participantUUID, not the deprecated participantId) +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); + +// Clear image (use imageId from drawImage response) +await zoomSdk.clearImage({ imageId: 'id-from-drawImage' }); + +// Clear webview (hides it — app continues running) +await zoomSdk.clearWebView(); +// Note: TypeDoc v0.16.36 shows no parameters. +// Guide examples show { webviewId: "xxx" } but this may be outdated. +``` + +## Coordinate System + +- **Origin:** Top-left corner (0, 0) +- **X:** Increases rightward +- **Y:** Increases downward +- **Units:** PixelValue — supports `"Npx"`, `"N%"`, or raw `number` + +### Immersive Mode + +- Coordinates are CSS pixels relative to the meeting canvas +- Automatic scaling for different window sizes + +### Camera Mode + +- Coordinates are raw pixels relative to `renderTarget` dimensions +- Default renderTarget: 1280×720 (configurable) +- Access via: `config.media.renderTarget.width` / `.height` + +```javascript +const config = await zoomSdk.config({ /* ... */ }); +const rtWidth = config.media.renderTarget.width; // e.g. 1280 +const rtHeight = config.media.renderTarget.height; // e.g. 720 +``` + +## Z-Index Layering + +``` +zIndex: 2+ ─ WebViews, interactive overlays (top) +zIndex: 1 ─ Participant videos +zIndex: 0 ─ Background images (bottom) +``` + +Higher zIndex values render on top. All three element types (participant, image, webview) share the same z-index space and can overlap. + +## Events + +### onRenderedAppOpened + +Fires when the rendering context is ready. Best signal that CEF is initialized in camera mode. + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', () => { + // Safe to call drawParticipant, drawImage, drawWebView +}); +``` + +### onMyMediaChange + +Fires when the user's video changes (camera switch, "Original ratio" toggle, "HD" toggle). Returns device pixel dimensions of the source video. + +```javascript +zoomSdk.addEventListener('onMyMediaChange', (event) => { + // event.media.video.width / height — device pixels of source video + // Redraw your layout if needed +}); +``` + +### Window Resize (Immersive Only) + +When the Zoom meeting window is resized, the app must move and resize participants/images. Not relevant to Camera Mode (fixed renderTarget). + +## Immersive Mode vs Camera Mode + +| Aspect | Immersive | Camera | +|--------|-----------|--------| +| Scope | Entire meeting view | User's camera only | +| drawParticipant | Any participant | Self only | +| drawImage | Yes | Yes | +| drawWebView | Yes | Yes | +| Who sees it | All participants | All see it on this user's feed | +| Browser engine | Standard WebView | CEF (Chromium Embedded Framework) | +| Rendering | On-screen | Off-screen (OSR) | +| Coordinate space | CSS pixels | Raw pixels (renderTarget) | +| Simultaneous | One immersive at a time | Can run with Presentation mode | + +## Camera Mode: CEF Race Condition + +Camera mode uses CEF which takes time to initialize. Draw calls may fail if called too early. + +**Best approach: Listen for `onRenderedAppOpened`:** + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', async () => { + await zoomSdk.drawWebView({ x: 0, y: 0, width: 1280, height: 720, zIndex: 2 }); +}); +``` + +**Fallback: Retry with exponential backoff:** + +```javascript +async function drawWithRetry(drawFn, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await drawFn(); + return; + } catch (error) { + if (i === maxRetries - 1) throw error; + await new Promise(r => setTimeout(r, 200 * Math.pow(2, i))); + } + } +} +``` + +**Alternative: Check running context:** + +```javascript +const { runningContext } = await zoomSdk.getRunningContext(); +if (runningContext === 'inCamera') { + // CEF is ready, safe to draw +} +``` + +## Performance Tips + +- Use `requestAnimationFrame` for animations +- Minimize draw calls (batch updates when possible) +- Pre-render complex backgrounds to a single canvas ImageData +- Keep zIndex values low (0-10 range) +- Clear unused elements to free resources +- Test on lower-end hardware +- For full-screen images: tile the screen (HiDPI limitation) + +## Example: Two-Person Podcast Layout + +```javascript +// Background +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +const gradient = ctx.createLinearGradient(0, 0, 1280, 720); +gradient.addColorStop(0, '#1a1a2e'); +gradient.addColorStop(1, '#16213e'); +ctx.fillStyle = gradient; +ctx.fillRect(0, 0, 1280, 720); +const imageData = ctx.getImageData(0, 0, 1280, 720); + +await zoomSdk.drawImage({ imageData, x: 0, y: 0, zIndex: 0 }); + +// Host (left) — person cutout removes background +await zoomSdk.drawParticipant({ + participantUUID: hostUUID, + x: 40, y: 100, width: 580, height: 500, + zIndex: 1, cutout: 'person' +}); + +// Guest (right) +await zoomSdk.drawParticipant({ + participantUUID: guestUUID, + x: 660, y: 100, width: 580, height: 500, + zIndex: 1, cutout: 'person' +}); +``` + +## Version History + +| Feature | Client Version | SDK Version | +|---------|---------------|-------------| +| Core Layers API | 5.9.0 | 0.16 | +| `cutout: "person"` | 5.9.3 | 0.16 | +| `cutout: "rectangle"` | 5.11.0 | 0.16 | +| `cutout: "circle"`, `"square"`, `"verticalRectangle"` | 5.11.3 | 0.16 | +| `drawWebView()` / `clearWebView()` | 5.10.6 | 0.16.11+ | +| Camera Mode | 5.13.1 | 0.16 | +| `cameraModeMirroring` | 5.13.5 | 0.16 | + +## Resources + +- **Layers API docs**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Using the API**: https://developers.zoom.us/docs/zoom-apps/guides/layers-using-api/ +- **Manipulating UI**: https://developers.zoom.us/docs/zoom-apps/guides/layers-manipulating-ui/ +- **Camera Mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/camera-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js +- **SDK TypeDoc**: https://appssdk.zoom.us/classes/ZoomSdk.ZoomSdk.html +- **Immersive example**: [../examples/layers-immersive.md](../examples/layers-immersive.md) +- **Camera example**: [../examples/layers-camera.md](../examples/layers-camera.md) diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/oauth.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/oauth.md new file mode 100644 index 00000000..239fc02d --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/oauth.md @@ -0,0 +1,202 @@ +# Zoom Apps SDK - OAuth Reference + +OAuth flows for Zoom Apps: web-based redirect, In-Client, and third-party. + +## Three OAuth Flows + +| Flow | UX | When to Use | +|------|----|-------------| +| **Web-based redirect** | Opens browser, redirect back | Initial install from Marketplace | +| **In-Client OAuth** | Popup inside Zoom, no redirect | Subsequent authorizations (best UX) | +| **Third-party OAuth** | External provider (Auth0, Google) | When your app needs non-Zoom auth | + +## PKCE (Required for All Flows) + +All Zoom Apps OAuth must use PKCE (Proof Key for Code Exchange): + +```javascript +const crypto = require('crypto'); + +// Generate PKCE pair +const verifier = crypto.randomBytes(32).toString('hex'); +const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + +// verifier: stored server-side (never exposed to client) +// challenge: sent with authorization request +``` + +## Flow 1: Web-Based OAuth (Initial Install) + +``` +User clicks "Add" in Marketplace + | + v +GET https://zoom.us/oauth/authorize + ?client_id=YOUR_CLIENT_ID + &response_type=code + &redirect_uri=YOUR_REDIRECT_URI + &code_challenge=CHALLENGE + &code_challenge_method=S256 + &state=RANDOM_STATE + | + v +User authorizes -> Zoom redirects to YOUR_REDIRECT_URI?code=AUTH_CODE&state=STATE + | + v +Backend validates state, exchanges code for tokens + | + v +Backend gets deeplink, redirects user to Zoom client +``` + +### Server Route Handler + +```javascript +app.get('/auth', async (req, res) => { + const { code, state } = req.query; + + // Validate state (CSRF protection) + if (state !== req.session.state) { + return res.status(403).send('Invalid state'); + } + + // Exchange code for tokens + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + const { access_token, refresh_token, expires_in } = tokenResponse.data; + + // Store tokens securely + req.session.tokens = { access_token, refresh_token, expires_at: Date.now() + expires_in * 1000 }; + + // Get deeplink to open app in Zoom + const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${access_token}` } } + ); + + res.redirect(deeplink.data.deeplink); +}); +``` + +## Flow 2: In-Client OAuth (Best UX) + +No browser redirect - authorization happens inside Zoom: + +```javascript +// Frontend +const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); + +zoomSdk.addEventListener('onAuthorized', async (event) => { + await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: event.code, state: event.state }) + }); +}); + +await zoomSdk.authorize({ codeChallenge, state }); +``` + +See **[In-Client OAuth example](../examples/in-client-oauth.md)** for complete implementation. + +## Token Exchange Endpoint + +``` +POST https://zoom.us/oauth/token + +Headers: + Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET) + +Parameters: + grant_type=authorization_code + code=AUTH_CODE + redirect_uri=YOUR_REDIRECT_URI + code_verifier=PKCE_VERIFIER +``` + +Response: +```json +{ + "access_token": "...", + "token_type": "bearer", + "refresh_token": "...", + "expires_in": 3600, + "scope": "zoomapp:inmeeting" +} +``` + +## Token Refresh + +Access tokens expire in 1 hour. Use refresh token to get new ones: + +```javascript +async function refreshTokens(refreshToken) { + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'refresh_token', + refresh_token: refreshToken + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + return response.data; // { access_token, refresh_token, expires_in } +} +``` + +**Note:** Refresh tokens are single-use. Each refresh returns a new refresh_token. + +## Deep Linking + +After web OAuth, get a deeplink to open your app in Zoom: + +```javascript +const response = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${accessToken}` } } +); + +const { deeplink } = response.data; +// Redirect user to this URL to open app in Zoom client +``` + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `zoomapp:inmeeting` | In-meeting functionality (most common) | +| `user:read` | Read user profile | +| `meeting:read` | Read meeting details | +| `meeting:write` | Create/modify meetings | + +## Token Storage Patterns + +| Pattern | When to Use | +|---------|-------------| +| **Redis** | Multi-instance production servers | +| **Session cookie** | Simple single-server apps | +| **Firestore** | Serverless (Firebase) | +| **Encrypted database** | Complex apps with user accounts | + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/zoom-apps/authentication/ +- **In-Client OAuth example**: [../examples/in-client-oauth.md](../examples/in-client-oauth.md) +- **oauth skill**: [../../oauth/SKILL.md](../../oauth/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/zmail-sdk.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/zmail-sdk.md new file mode 100644 index 00000000..f330e961 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/references/zmail-sdk.md @@ -0,0 +1,214 @@ +# Zoom Mail Integration + +Integrate with Zoom Mail via REST API and Zoom Apps SDK for mail plugins. + +## Overview + +**Important**: There is no standalone "ZMail JS SDK". Zoom Mail integration uses: +1. **Zoom Mail REST API** - Server-side email operations +2. **Zoom Apps SDK** - Build mail plugins that run inside Zoom client + +## Prerequisites + +- Zoom Workplace Pro, Standard Pro, Business, or Enterprise account +- Zoom Mail enabled (Pro accounts have it by default; Business/Enterprise must enable) +- OAuth app with mail scopes + +## Zoom Mail REST API + +### Base URL + +``` +https://api.zoom.us/v2/emails +``` + +### Key Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/emails/mailboxes/{email}/messages/send` | Send a message | +| POST | `/emails/mailboxes/{email}/messages` | Create/add message to mailbox | +| POST | `/emails/mailboxes/{email}/drafts` | Create draft | +| POST | `/emails/mailboxes/{email}/labels` | Create label | +| GET | `/emails/mailboxes/me/profile` | Get mailbox profile | + +### Send Email Example + +Emails must be in RFC 2822 format, base64 encoded: + +```javascript +// Generate RFC 2822 compliant message +function generateEmailMessage(from, to, subject, body) { + const message = `From: ${from}\nTo: ${to}\nSubject: ${subject}\n\n${body}`; + return btoa(message); // Base64 encode +} + +// Send email via API +async function sendEmail(mailbox, toEmail, subject, body) { + const encodedMessage = generateEmailMessage(mailbox, toEmail, subject, body); + + const response = await fetch( + `https://api.zoom.us/v2/emails/mailboxes/${mailbox}/messages/send`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + raw: encodedMessage + }) + } + ); + + return response.json(); +} + +// Usage +await sendEmail( + 'sender@company.zmail.com', + 'recipient@example.com', + 'Meeting Follow-up', + 'Thank you for attending today\'s meeting.' +); +``` + +### Create Draft + +```javascript +async function createDraft(mailbox, to, subject, body) { + const encodedMessage = generateEmailMessage(mailbox, to, subject, body); + + await fetch( + `https://api.zoom.us/v2/emails/mailboxes/${mailbox}/drafts`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + raw: encodedMessage + }) + } + ); +} +``` + +### Get Mailbox Profile + +```javascript +const profile = await fetch( + 'https://api.zoom.us/v2/emails/mailboxes/me/profile', + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } +).then(r => r.json()); + +console.log('Email:', profile.email); +``` + +## Zoom Apps SDK - Mail Plugins + +Build apps that run inside Zoom Mail tab using Zoom Apps SDK. + +### Installation + +```bash +npm install @zoom/appssdk +``` + +Or via CDN: +```html + +``` + +### Initialize for Mail Context + +```javascript +// IMPORTANT: Do NOT declare "let zoomSdk" - causes redeclaration error +// The SDK defines window.zoomSdk globally +let sdk = window.zoomSdk; + +async function init() { + try { + const configResponse = await sdk.config({ + popout: true, + capabilities: ['insertContentToMailActiveEditor'], + version: '0.16' + }); + + console.log('Running in context:', configResponse.runningContext); + // Will be "mailTab" when running in Zoom Mail + } catch (error) { + console.error('Not running inside Zoom client:', error); + } +} +``` + +### Insert Content to Mail Editor + +Insert HTML content into the active mail composer: + +```javascript +// Insert content into mail editor (must comply with Tiptap HTML specs) +await sdk.insertContentToMailActiveEditor({ + html: '

This content will be inserted into the email body.

' +}); +``` + +### Mail Plugin Use Cases + +| Use Case | Description | +|----------|-------------| +| **Email Templates** | Insert pre-built email templates | +| **Signature Manager** | Dynamic email signatures | +| **Meeting Links** | Auto-insert Zoom meeting links | +| **CRM Integration** | Pull contact info into emails | +| **Translation** | Translate email content | + +## Authentication + +### Server-to-Server OAuth (for REST API) + +```javascript +async function getAccessToken() { + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`), + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'grant_type=client_credentials' + }); + + const data = await response.json(); + return data.access_token; // Valid for 1 hour +} +``` + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `mail:read` | Read mailbox data | +| `mail:write` | Send emails, create drafts | +| `mail:read:admin` | Admin read access | +| `mail:write:admin` | Admin write access | + +## Limitations + +| Limitation | Notes | +|------------|-------| +| Account requirement | Zoom Workplace Pro+ with Zoom Mail | +| Email format | Must be RFC 2822 compliant, base64 encoded | +| Plugin context | Zoom Apps SDK mail features only work in Mail tab | +| HTML in editor | Must comply with Tiptap specifications | + +## Resources + +- **Zoom Mail API Docs**: https://developers.zoom.us/docs/api/rest/zoom-mail/ +- **Zoom Mail Overview**: https://developers.zoom.us/docs/mail/ +- **Zoom Apps SDK**: https://github.com/zoom/appssdk +- **NPM Package**: https://www.npmjs.com/package/@zoom/appssdk +- **Sample Implementation**: https://github.com/Wrightlab1/ZoomMailAPI diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/common-issues.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..39816fb8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/common-issues.md @@ -0,0 +1,85 @@ +# Common Issues + +Quick diagnostics for Zoom Apps SDK problems. + +## Diagnostic Table + +| Issue | Cause | Solution | +|-------|-------|----------| +| App shows blank panel | Domain not in allowlist | Marketplace > Feature > Add Allow List > add your domain | +| `zoomSdk is not defined` | SDK script not loaded | Add `` | +| `SyntaxError: redeclaration of non-configurable global property` | Declared `let zoomSdk` | Use `let sdk = window.zoomSdk` instead | +| `config()` throws error | Not running in Zoom client | Wrap in try/catch, show browser fallback UI | +| `config()` hangs forever | SDK not initialized | Add 3-second timeout fallback | +| API call fails silently | Missing OAuth scope | Add required scopes in Marketplace > Scopes tab | +| `authorize()` doesn't work | Wrong OAuth flow | Use In-Client OAuth: `authorize()` + `onAuthorized` | +| `postMessage` not received | Instances not connected | Call `connect()` first, wait for `onConnect` | +| `runRenderingContext` fails | Missing capability | Add `'runRenderingContext'` to config() capabilities | +| App works then stops | Tunnel URL changed (ngrok/cloudflared/etc.) | Update every Marketplace URL that points at your tunnel domain (home page, redirect URLs, etc.) | +| CORS errors | Backend URL mismatch | Ensure frontend fetches from same-origin or configure CORS | +| Cookies not persisting | Wrong cookie settings | Set `sameSite: 'none', secure: true` | +| OAuth token exchange 400 | Wrong redirect URI | Ensure redirect URI matches exactly in Marketplace and code | +| 403 from Zoom REST API | Token expired | Implement token refresh with `refresh_token` | +| App loads in browser but not in Zoom | CSP headers wrong | Add `frame-ancestors 'self' zoom.us *.zoom.us` | +| `unsupportedApis` contains your API | Old Zoom client | User needs to update Zoom client | +| Collaborate/Layers APIs missing | Host privileges or client/version mismatch | Check `unsupportedApis` + `clientVersion`; ensure required features enabled in Marketplace; test as meeting host where required | +| drawImage fails in camera mode | CEF not ready | Add retry with exponential backoff (see camera-mode example) | + +## Error Codes + +Common SDK error codes from `config()` and API calls: + +| Code | Meaning | Fix | +|------|---------|-----| +| `INVALID_PARAMETERS` | Wrong argument format | Check API docs for correct parameter shape | +| `NOT_SUPPORTED` | API not available in this context | Check running context and unsupportedApis | +| `PERMISSION_DENIED` | Missing capability or scope | Add to config() capabilities AND Marketplace scopes | +| `INTERNAL_ERROR` | SDK internal failure | Retry, check Zoom client version | +| `NOT_CONFIGURED` | config() not called yet | Call config() before any other SDK method | + +## Quick Diagnostic Workflow + +``` +App doesn't work? + │ + ├─ Blank panel, no errors? + │ └─ Domain allowlist. Check Marketplace > Feature > Add Allow List + │ + ├─ JavaScript error in console? + │ ├─ "redeclaration" → Use `let sdk = window.zoomSdk` + │ ├─ "not defined" → SDK script not loaded + │ └─ "not configured" → Call config() first + │ + ├─ config() fails? + │ ├─ In browser → Expected. Not running in Zoom. + │ └─ In Zoom → Check capabilities match scopes + │ + ├─ API call returns error? + │ ├─ PERMISSION_DENIED → Add scope in Marketplace + │ ├─ NOT_SUPPORTED → Wrong running context + │ └─ INVALID_PARAMETERS → Check argument format + │ + └─ Works locally, fails after deploy? + ├─ Domain allowlist updated? + ├─ HTTPS configured? + └─ Cookies: SameSite=None, Secure=true? +``` + +## Enable SDK Debug Logging + +```javascript +// Check supported APIs at runtime +const { supportedApis } = await zoomSdk.getSupportedJsApis(); +console.log('Supported APIs:', supportedApis); + +// Check what was unsupported after config +const config = await zoomSdk.config({...}); +console.log('Unsupported:', config.unsupportedApis); +console.log('Client version:', config.clientVersion); +console.log('Running context:', config.runningContext); +``` + +## Resources + +- **Debugging guide**: [debugging.md](debugging.md) +- **Migration guide**: [migration.md](migration.md) diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/debugging.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/debugging.md new file mode 100644 index 00000000..5417beb7 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/debugging.md @@ -0,0 +1,150 @@ +# Debugging Guide + +Local development setup, ngrok configuration, and browser preview. + +## ngrok Setup + +ngrok provides an HTTPS tunnel to your local server. Required because Zoom Apps need HTTPS. + +```bash +# Install ngrok (https://ngrok.com) +# Then start tunnel: +ngrok http 3000 +``` + +Copy the `https://xxxxx.ngrok.io` URL. + +### ngrok Free Tier Limitation + +Free ngrok URLs **change every restart**. You must update 4 places in Marketplace each time: + +1. **Home URL**: `https://xxxxx.ngrok.io` +2. **Redirect URL**: `https://xxxxx.ngrok.io/auth` +3. **OAuth Allow List**: `https://xxxxx.ngrok.io` +4. **Domain Allow List**: `xxxxx.ngrok.io` (no protocol) + +**Tip:** Get ngrok paid plan ($8/mo) for a stable subdomain (e.g., `https://myapp.ngrok.io`). + +## Marketplace Configuration for Local Dev + +In [Zoom Marketplace](https://marketplace.zoom.us/) -> Your App: + +### Feature tab -> Zoom App +- **Home URL**: `https://xxxxx.ngrok.io` +- **Share URL** (optional): `https://xxxxx.ngrok.io` + +### Feature tab -> OAuth Redirect URL +- **Redirect URL for OAuth**: `https://xxxxx.ngrok.io/auth` +- **Add Allow List**: `https://xxxxx.ngrok.io` + +### Feature tab -> Domain Allow List +- `xxxxx.ngrok.io` +- `appssdk.zoom.us` (if using CDN) + +### Scopes tab +- `zoomapp:inmeeting` (minimum required) + +## Browser Preview Mode + +Test your UI outside Zoom by implementing a fallback: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +let isInZoom = false; + +async function init() { + try { + const config = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'getUserContext'], + version: '0.16' + }); + isInZoom = true; + initZoomApp(config); + } catch (error) { + isInZoom = false; + initBrowserPreview(); + } +} + +function initBrowserPreview() { + // Show mock UI with sample data for development + const mockMeeting = { meetingID: '123456789', meetingTopic: 'Test Meeting' }; + const mockUser = { screenName: 'Dev User', role: 'host' }; + renderApp(mockMeeting, mockUser); + console.log('Running in browser preview mode'); +} +``` + +## Opening DevTools in Zoom + +The Zoom client's embedded browser supports DevTools: + +### Windows +1. Open Zoom client +2. Open your Zoom App +3. Right-click inside the app panel +4. Select "Inspect Element" (if available) + +### Alternative: Remote Debugging +1. Start Zoom with remote debugging flag +2. Open `chrome://inspect` in Chrome +3. Find your app's WebView + +**Note:** DevTools availability depends on Zoom client version and settings. Not all surfaces support it. + +## Environment Variables + +Use `.env` file with `dotenv`: + +```bash +# .env (never commit this file) +ZOOM_APP_CLIENT_ID=abc123 +ZOOM_APP_CLIENT_SECRET=xyz789 +ZOOM_APP_REDIRECT_URI=https://xxxxx.ngrok.io/auth +SESSION_SECRET=random-secret-string-here +ZOOM_HOST=https://zoom.us +``` + +```javascript +// server.js +require('dotenv').config(); +// process.env.ZOOM_APP_CLIENT_ID is now available +``` + +Add `.env` to `.gitignore`: +``` +.env +.env.local +``` + +## Common Dev Workflow + +```bash +# Terminal 1: Start server +npm run dev + +# Terminal 2: Start ngrok +ngrok http 3000 + +# Then: +# 1. Copy ngrok URL +# 2. Update Marketplace URLs (if changed) +# 3. Open Zoom client +# 4. Click your app in sidebar or during meeting +# 5. Check the ngrok local request inspector for request logs +``` + +## ngrok Request Inspector + +ngrok provides a local web UI that shows: +- All HTTP requests to your tunnel +- Request/response headers and bodies +- Replay failed requests + +This is invaluable for debugging OAuth redirects and API calls. + +## Resources + +- **Testing guide**: https://developers.zoom.us/docs/zoom-apps/guides/testing/ +- **ngrok docs**: https://ngrok.com/docs diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md new file mode 100644 index 00000000..4e26d838 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md @@ -0,0 +1,39 @@ +--- +title: "Forum-Derived Top Questions (Zoom Apps)" +--- + +# Forum-Derived Top Questions (Zoom Apps) + +This is a checklist of the most common forum questions for **Zoom Apps SDK**. + +## Fast Routing Questions (Ask First) + +- Running context: `inMeeting` vs `inMainClient` vs `inWebinar` vs `inImmersive` etc. +- SDK loading style: **NPM import** vs **CDN** (`window.zoomSdk`) +- Marketplace config: domain allowlist, scopes, and required capabilities + +## “App won’t load / blank panel” + +Most common causes: +- domain not in Marketplace allowlist +- trying to run the app in a normal browser (needs preview/demo mode) +- blocked mixed-content or missing HTTPS in dev tunnel + +## “zoomSdk redeclaration” (CDN) + +Common failure: +- redeclaring `let zoomSdk = ...` when CDN already defines `window.zoomSdk` + +Answer pattern: +- use `const sdk = window.zoomSdk` or NPM import + +## Auth Confusion + +Common asks: +- “Do I use OAuth redirects?” +- “How does In-Client OAuth work?” + +Answer pattern: +- explain In-Client OAuth (PKCE) and required scopes +- differentiate from REST API OAuth flows + diff --git a/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/migration.md b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/migration.md new file mode 100644 index 00000000..8c000f18 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-apps-sdk/troubleshooting/migration.md @@ -0,0 +1,106 @@ +# SDK Version Migration + +Notes on upgrading @zoom/appssdk versions and handling deprecations. + +## Current Version + +**Recommended:** `@zoom/appssdk` v0.16.26+ (latest stable) + +```json +{ + "dependencies": { + "@zoom/appssdk": "^0.16.26" + } +} +``` + +## Version Pinning Strategy + +| Strategy | package.json | Risk | Use When | +|----------|-------------|------|----------| +| **Exact** | `"0.16.26"` | Lowest | Production apps, critical stability | +| **Patch** | `"~0.16.26"` | Low | Most apps | +| **Minor** | `"^0.16.26"` | Medium | Active development | + +## Checking API Availability at Runtime + +Not all APIs are available in all Zoom client versions. Always check: + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); + +// Check before using an API +if (supportedApis.includes('authorize')) { + // Safe to use In-Client OAuth + await zoomSdk.authorize({...}); +} else { + // Fall back to web redirect OAuth + window.location.href = '/install'; +} +``` + +Also check `unsupportedApis` after `config()`: + +```javascript +const config = await zoomSdk.config({ + capabilities: ['authorize', 'getMeetingContext', 'newFeature'], + version: '0.16' +}); + +if (config.unsupportedApis.includes('newFeature')) { + console.log('newFeature not available in this client version'); + // Graceful degradation +} +``` + +## Config Version Parameter + +The `version` parameter in `config()` indicates which SDK API version you expect: + +```javascript +await zoomSdk.config({ + capabilities: [...], + version: '0.16' // API version, not NPM package version +}); +``` + +This helps Zoom maintain backward compatibility. Use the latest supported version. + +## Sample App SDK Versions + +Status of official sample repositories: + +| Repository | SDK Version | Status | Notes | +|-----------|-------------|--------|-------| +| zoomapps-sample-js | ^0.16.26 | Current | Best reference | +| zoomapps-advancedsample-react | 0.16.0 | Outdated | Works but update recommended | +| zoomapps-customlayout-js | ^0.16.8 | Outdated | Layers API may differ | +| zoomapps-texteditor-vuejs | ^0.16.7 | Outdated | Y.js pattern still valid | +| zoomapps-serverless-vuejs | ^0.16.21 | Slightly outdated | Firebase pattern still valid | + +## Deprecation Pattern + +Zoom typically deprecates APIs gradually: +1. API marked deprecated in docs +2. `unsupportedApis` starts returning it in newer clients +3. API stops working in future client versions + +**Best practice:** Check `getSupportedJsApis()` at startup and implement fallbacks. + +## Migration Checklist + +When upgrading SDK version: + +- [ ] Update `@zoom/appssdk` in package.json +- [ ] Run `npm install` +- [ ] Check changelog for breaking changes +- [ ] Test all capabilities in Zoom client +- [ ] Verify `getSupportedJsApis()` includes your APIs +- [ ] Test in both meeting and main client contexts +- [ ] Test browser preview fallback still works +- [ ] Update `version` parameter in `config()` if needed + +## Resources + +- **NPM package**: https://www.npmjs.com/package/@zoom/appssdk +- **Changelog**: https://github.com/zoom/appssdk/releases diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/RUNBOOK.md b/partner-built/zoom-plugin/skills/zoom-mcp/RUNBOOK.md new file mode 100644 index 00000000..60e57c47 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/RUNBOOK.md @@ -0,0 +1,66 @@ +# zoom-mcp RUNBOOK — 5-Minute Preflight + +Quick diagnostic checklist before using the Zoom MCP server. + +## Preflight Checklist + +**1. Token exported for the bundled connector?** +```bash +echo "${ZOOM_MCP_ACCESS_TOKEN:+set}" +echo "${ZOOM_DOCS_MCP_ACCESS_TOKEN:+set}" +``` +If empty, export the relevant token using [concepts/oauth-setup.md](concepts/oauth-setup.md). + +**2. Tool discovery working?** +- Confirm the client can see `recordings_list`, `search_meetings`, `get_meeting_assets`, + and `get_recording_resource`. +- For the dedicated Zoom Docs server, confirm the client can see `create_file_with_content` + and `get_file_content`. +- If your client exposes raw protocol inspection, verify `tools/list` succeeds. +- Compare the visible tools with [references/tools.md](references/tools.md). + +**3. Correct OAuth scopes on the token?** + +Minimum Zoom MCP scopes for this guide: +- `ai_companion:read:search` — Search across Zoom Meeting, Zoom Chat, and Zoom Doc +- `meeting:read:search` — Search and view meetings +- `meeting:read:assets` — View a meeting's assets +- `cloud_recording:read:list_user_recordings` — Lists all cloud recordings for a user. +- `cloud_recording:read:content` — read recording content scope + +Minimum Zoom Docs MCP scopes: +- `docs:write:import` — Create new file by import +- `docs:read:export` — Read file content in Markdown format + +Whiteboard uses a separate scope set. See [whiteboard/SKILL.md](whiteboard/SKILL.md). + +**4. AI Companion features enabled?** + +Go to Zoom web portal → **Admin → Account Management → Account Settings → AI Companion**. +Confirm **Smart Recording** and **Meeting Summary** are enabled if you expect semantic search, +meeting assets, or transcript-rich recording content to be useful. + +## Quick Fixes + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| `-32001 Access token is required` | Token env var missing or empty | Export `ZOOM_MCP_ACCESS_TOKEN`, then restart Claude Code | +| `-32001 Invalid access token, does not contain scopes:[meeting:read:search]` | Missing semantic-search scope | Add `meeting:read:search` and mint a new user token | +| `-32001 Invalid access token, does not contain scopes:[meeting:read:assets,...]` | Missing meeting-assets scope | Add `meeting:read:assets` and mint a new user token | +| `-32001 Invalid access token, does not contain scopes:[cloud_recording:read:list_user_recordings,...]` | Missing recordings-list scope | Add `cloud_recording:read:list_user_recordings` | +| `-32001 Invalid access token, does not contain scopes:[cloud_recording:read:content]` | Missing recording-content scope | Add `cloud_recording:read:content` | +| `-32602 Can not found tool: ... in this MCP Server` | Wrong endpoint surface or wrong tool name | Re-run `tools/list` and use the current tool names for the active MCP server | +| `-32603 Call handle error` | Missing required parameters or server-side call handling failure | Re-check required arguments against the live schema and retry | +| `Upstream API returned error status code: 400 ... invalid param` | Invalid parameter value passed through to the underlying Zoom API | Fix the specific argument value, such as `parent_id` for Docs creation on the dedicated Docs server | +| Search returns no useful meeting content | AI Companion features missing or data not indexed | Enable Smart Recording + Meeting Summary, widen the search window, or fall back to `recordings_list` | + +## Auth Reality Check + +Use user OAuth as the documented execution path for Zoom MCP content tools in this plugin. +Do not rely on Server-to-Server OAuth as a supported MCP auth model here. + +## Where to Get Help + +- Developer forum: https://devforum.zoom.us/ +- Zoom support: https://support.zoom.com/ +- MCP protocol docs: https://modelcontextprotocol.io/ diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/SKILL.md b/partner-built/zoom-plugin/skills/zoom-mcp/SKILL.md new file mode 100644 index 00000000..cf6702e8 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/SKILL.md @@ -0,0 +1,229 @@ +--- +name: zoom-mcp +description: Guidance for the bundled Zoom MCP connectors. Use after routing to an MCP workflow when planning or troubleshooting tool-based access to meetings, recordings, meeting assets, or transcripts. Route Zoom Docs requests to the dedicated Docs MCP server and Whiteboard-specific requests to `zoom-mcp/whiteboard`. +user-invocable: false +triggers: + - "zoom mcp" + - "zoom mcp server" + - "zoom mcp tools" + - "zoom tools/list" + - "zoom tools/call" + - "ai companion transcript" + - "agentic retrieval" + - "zoom semantic meeting search" + - "zoom search meetings by content" + - "zoom meeting assets mcp" + - "zoom recording resource mcp" + - "zoom docs via mcp" + - "zoom transcript via mcp" + - "meeting transcript via mcp" +--- + +# Zoom MCP + +Guidance for the bundled Zoom MCP connector in this Claude plugin. Prefer `design-mcp-workflow` or [setup-zoom-mcp](../setup-zoom-mcp/SKILL.md) first, then route here for tool-surface details, auth expectations, and MCP-specific constraints. + +# Zoom MCP Server + +This plugin bundles Zoom's hosted MCP server at `mcp-us.zoom.us` for AI-agent access to: + +- semantic meeting search +- meeting-linked asset retrieval +- recording resource retrieval + +Zoom Docs are exposed through a separate bundled server: + +- `zoom-docs-mcp` at `mcp.zoom.us` +- purpose-built for Zoom Docs creation and retrieval + +Current tool names from the main Zoom MCP server: + +- `get_meeting_assets` +- `search_meetings` +- `get_recording_resource` +- `recordings_list` + +Some MCP clients namespace server tools in the UI, for example `zoom-mcp:recordings_list`. +Treat the raw tool names above as authoritative. + +Zoom Docs-specific MCP work should use the dedicated `zoom-docs-mcp` server. + +Whiteboard-specific MCP work is covered by the dedicated skill +[whiteboard/SKILL.md](whiteboard/SKILL.md). + +## Quick Start + +**1. Export the token expected by the bundled connector:** + +```bash +export ZOOM_MCP_ACCESS_TOKEN="your_zoom_user_oauth_access_token" +``` + +**2. Enable or restart the plugin so Claude restarts the bundled MCP server definition.** + +**3. Verify discovery:** +- Confirm the client can see `recordings_list`, `search_meetings`, `get_meeting_assets`, + and `get_recording_resource`. +- If the client exposes raw protocol inspection, `tools/list` is the authoritative discovery source. +- The current catalog is documented in [references/tools.md](references/tools.md). + +**4. Run the first useful call:** +```text +recordings_list + userId: "me" + from: "2026-03-01" + to: "2026-03-06" + page_size: 10 +``` + +## Critical Notes + +**1. User OAuth is the documented execution path** + +Use a **General app** with **user-level OAuth** as the execution path for Zoom MCP +tool use in this plugin. Do not rely on Server-to-Server OAuth as a supported MCP auth model here. + +**2. Zoom MCP uses MCP-specific granular scopes** + +The Zoom MCP scope set is not the same as the older broad REST scopes. +The key scopes for the main Zoom MCP server are: +- `ai_companion:read:search` — Search across Zoom Meeting, Zoom Chat, and Zoom Doc, returning the most relevant results based on the query +- `meeting:read:search` — Search and view meetings +- `meeting:read:assets` — View a meeting's assets +- `cloud_recording:read:list_user_recordings` — Lists all cloud recordings for a user. +- `cloud_recording:read:content` — read recording content scope +- `docs:write:import` — Create a new file by import +- `docs:read:export` — Read file content in Markdown format + +For Zoom Docs MCP specifically, the official docs page shows these granular scopes for the documented tools: +- `docs:write:import` — Create a new file by import +- `docs:read:export` — Read file content in Markdown format + +**3. AI Companion features are feature prerequisites, not scope substitutes** + +Semantic meeting search, meeting assets, and recording-content retrieval depend on account +features such as **Smart Recording** and **Meeting Summary** for useful results. These feature +settings do not replace the required OAuth scopes. + +**4. Whiteboard is a separate MCP surface** + +The Zoom MCP endpoint and the Whiteboard MCP endpoint are separate. Route Whiteboard-specific +requests to [whiteboard/SKILL.md](whiteboard/SKILL.md). + +**5. Use REST for deterministic meeting CRUD** + +The current Zoom MCP tool surface does not expose deterministic +meeting create, update, or delete tools. If the user needs explicit meeting CRUD operations, +route to [../rest-api/SKILL.md](../rest-api/SKILL.md). + +## Server Endpoints + +| Transport | URL | +|-----------|-----| +| Streamable HTTP (recommended) | `https://mcp-us.zoom.us/mcp/zoom/streamable` | +| SSE (fallback) | `https://mcp-us.zoom.us/mcp/zoom/sse` | + +Dedicated Docs MCP server: + +| Transport | URL | +|-----------|-----| +| Streamable HTTP (recommended) | `https://mcp.zoom.us/mcp/docs/streamable` | +| SSE (fallback) | `https://mcp.zoom.us/mcp/docs/sse` | + +Dedicated Whiteboard MCP skill: +- [whiteboard/SKILL.md](whiteboard/SKILL.md) + +## Search and Retrieval Model + +`search_meetings` uses AI Companion retrieval rather than a plain metadata filter. In this +use the live MCP server as authoritative for response schema and scope behavior. + +Two result families matter most: + +- **Recap-oriented results**: AI summary, meeting-linked documents, recordings, and related assets +- **Recording-oriented results**: cloud recording references and transcript-capable resources + +Use [examples/transcript-retrieval.md](examples/transcript-retrieval.md) for the main retrieval +workflow. + +## Tool Catalog + +| Tool | Key Parameters | Required Scope | +|------|---------------|----------------| +| `get_meeting_assets` | `meetingId`* | `meeting:read:assets` | +| `search_meetings` | `q`, `from`, `to`, `page_size`, `next_page_token` | `meeting:read:search` | +| `get_recording_resource` | `meetingId`*, `types`, `clip_num`, `play_time`, `raw_passcode`, `encode_passcode` | `cloud_recording:read:content` | +| `recordings_list` | `userId`*, `from`, `to`, `meeting_id`, `trash`, `trash_type`, `page_size`, `next_page_token` | `cloud_recording:read:list_user_recordings` | + +\* Required parameter + +Full parameter and output guidance: [references/tools.md](references/tools.md) + +## Key Workflows + +**Search meeting content, then retrieve assets:** +```text +search_meetings + q: "Q4 planning discussion" + from: "2026-03-01" + to: "2026-03-06" +→ choose a returned meeting +→ get_meeting_assets meetingId: "MEETING_ID_OR_UUID" +``` + +**List recordings, then retrieve recording resources:** +```text +recordings_list + userId: "me" + from: "2026-03-01" + to: "2026-03-06" +→ choose a recording target +→ get_recording_resource meetingId: "MEETING_UUID_OR_RECORDING_ID" +``` + +**Create or fetch a Zoom Doc:** +- use the dedicated `zoom-docs-mcp` server rather than the main `zoom-mcp` server +- official documented tools on the Zoom Docs MCP page are: + - `create_file_with_content` + - `get_file_content` + +## Error Reference + +| Code | Meaning | Fix | +|------|---------|-----| +| `401 Unauthorized` | Missing or rejected bearer token at the endpoint | Set `ZOOM_MCP_ACCESS_TOKEN`, then restart Claude or re-enable the plugin | +| `-32001 Invalid access token` | Token expired, malformed, or missing required scopes | Refresh OAuth token and verify the MCP-specific scopes | +| `-32602 Can not found tool` | Requested tool name is not exposed by the active MCP server | Re-run `tools/list` and use the current tool names for that endpoint | +| `404` | Possible downstream resource-not-found response | Re-discover the target with `search_meetings` or `recordings_list` | + +Full error reference: [references/error-codes.md](references/error-codes.md) + +## Documentation + +### Concepts +- [concepts/mcp-architecture.md](concepts/mcp-architecture.md) — MCP protocol, hosted endpoints, discovery, and capability model +- [concepts/oauth-setup.md](concepts/oauth-setup.md) — OAuth app creation, MCP-specific scopes, AI Companion prerequisites, token lifecycle + +### Examples +- [examples/transcript-retrieval.md](examples/transcript-retrieval.md) — Search/assets and recording-resource workflows +- [examples/create-zoom-doc.md](examples/create-zoom-doc.md) — Verified Zoom Docs creation flow +- [examples/search-and-act.md](examples/search-and-act.md) — Search, inspect assets, and hand off CRUD work to REST when needed +- [examples/meeting-lifecycle.md](examples/meeting-lifecycle.md) — Why meeting CRUD belongs in REST, plus the MCP-to-REST handoff pattern + +### References +- [references/tools.md](references/tools.md) — Current Zoom MCP tool reference +- [references/error-codes.md](references/error-codes.md) — MCP and Zoom API errors with fixes +- [whiteboard/SKILL.md](whiteboard/SKILL.md) — Dedicated Whiteboard MCP skill + +### Troubleshooting +- [troubleshooting/common-errors.md](troubleshooting/common-errors.md) — Scope failures, endpoint mixups, search/recording issues + +### Operations +- [RUNBOOK.md](RUNBOOK.md) — 5-minute preflight and debugging checklist + +## Related Skills + +- [zoom-rest-api](../rest-api/SKILL.md) — Deterministic REST API access, including meeting CRUD +- [zoom-oauth](../oauth/SKILL.md) — OAuth implementation patterns +- [zoom-webhooks](../webhooks/SKILL.md) — Event-driven recording and meeting workflows +- [zoom-rtms](../rtms/SKILL.md) — Live media and transcript streams during active meetings diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/concepts/mcp-architecture.md b/partner-built/zoom-plugin/skills/zoom-mcp/concepts/mcp-architecture.md new file mode 100644 index 00000000..90c4bff3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/concepts/mcp-architecture.md @@ -0,0 +1,94 @@ +# MCP Architecture — Zoom MCP Server + +## What is MCP? + +Model Context Protocol (MCP) standardizes how AI systems connect to external tools and data +sources. Zoom exposes hosted MCP surfaces that clients can discover and call over MCP. + +## Hosted Zoom MCP Surfaces + +### Zoom MCP + +| Transport | URL | +|-----------|-----| +| Streamable HTTP (recommended) | `https://mcp-us.zoom.us/mcp/zoom/streamable` | +| SSE (fallback) | `https://mcp-us.zoom.us/mcp/zoom/sse` | + +### Whiteboard MCP + +| Transport | URL | +|-----------|-----| +| Streamable HTTP (recommended) | `https://mcp-us.zoom.us/mcp/whiteboard/streamable` | +| SSE (fallback) | `https://mcp-us.zoom.us/mcp/whiteboard/sse` | + +Whiteboard MCP is covered by the dedicated skill +[../whiteboard/SKILL.md](../whiteboard/SKILL.md). + +## Discovery Model + +Do not hardcode tool counts in client logic. + +Use the MCP protocol `tools/list` response as the current source of truth for: +- tool names +- descriptions +- parameter schemas +- newly added or removed tools + +## Current Capability Shape + +The current Zoom MCP surface is centered on: +- semantic meeting search +- meeting asset retrieval +- recording resource retrieval +- Zoom Docs creation from Markdown + +If the task requires deterministic meeting CRUD, use the REST API skill instead of assuming +those operations exist on the current Zoom MCP surface. + +## Authentication Model + +User OAuth is the primary documented path. +Use user OAuth as the expected auth model for the bundled Zoom MCP servers in this plugin. + +## Protected Resource Metadata + +The hosted MCP surfaces advertise supported scopes through OAuth protected-resource metadata. +Zoom MCP protected-resource metadata currently exposes: +- `ai_companion:read:search` +- `meeting:read:assets` +- `meeting:read:search` +- `cloud_recording:read:content` +- `cloud_recording:read:list_user_recordings` +- `docs:write:import` +- `docs:read:export` + +Whiteboard MCP protected-resource metadata currently exposes: +- `whiteboard:write:whiteboard` +- `whiteboard:read:list_whiteboards` +- `whiteboard:read:whiteboard` + +## Retrieval Model + +`search_meetings` is not just a title filter. It is a semantic retrieval path over meeting +content, recap-linked assets, and recording-linked artifacts. + +Useful result families: +- recap-oriented results with AI summaries and linked assets +- recording-oriented results for post-meeting content retrieval + +When writing parsers, validate the live response shape from the server rather than relying on +older example field names. + +## Feature Prerequisites + +AI Companion features such as **Smart Recording** and **Meeting Summary** are feature +prerequisites for useful semantic retrieval and recap-linked content. They do not replace the +required OAuth scopes. + +## Error Layering + +Failures can happen at two layers: +- MCP protocol layer (`-32001`, `-32602`, `-32603`) +- underlying Zoom API-style permission/resource failures surfaced through the MCP response + +See [../references/error-codes.md](../references/error-codes.md). diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/concepts/oauth-setup.md b/partner-built/zoom-plugin/skills/zoom-mcp/concepts/oauth-setup.md new file mode 100644 index 00000000..53c3e8e4 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/concepts/oauth-setup.md @@ -0,0 +1,202 @@ +# OAuth Setup — Zoom MCP Server + +## Overview + +The documented path for Zoom MCP is a **General app** using **user-level OAuth**. +Each user authorizes with their own Zoom account, and the resulting bearer token is passed by +the bundled connector in [`.mcp.json`](../../../.mcp.json). + +## Step 1: Create a General App with User-Level OAuth + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) → **Develop** → **Build App**. +2. Create a **General app**. +3. Configure the app for **user-level OAuth** for the per-user MCP path. +4. Set a redirect URL for your client or local test environment. +5. Note the client ID and client secret. + +### If You Do Not Already Have a Redirect Endpoint + +For development, two pragmatic options are: + +**Option 0: `localhost` and manually copy the code** + +Example: + +```text +http://localhost:3000/oauth/zoom/callback +``` + +If the local app does not actually handle the callback yet, the browser may show a failed page +load. You can still copy the `code` and `state` values from the browser URL and paste them into +Claude or your terminal flow so the token exchange can continue manually. + +Pros: +- no tunnel and no third-party capture service +- fastest setup if you only need the authorization code once +- keeps the authorization code on your machine + +Cons: +- manual copy/paste step every time +- no automatic code exchange or refresh flow +- less convenient if you repeat the auth flow often + +**Option 1: `ngrok` in front of a local callback server** + +Example: + +```text +Local app: http://localhost:3000/oauth/zoom/callback +Public redirect URL: https://your-subdomain.ngrok.app/oauth/zoom/callback +``` + +Pros: +- best match for the real OAuth flow because your own app receives the callback directly +- easy to inspect requests locally while keeping your app logic in one place +- useful if you also need to exchange the code for tokens automatically + +Cons: +- requires running a local server plus the tunnel +- slightly more setup than a capture-only endpoint +- you are exposing a local service to the internet, so keep the callback narrow and temporary + +**Option 2: `webhook.site` for one-off callback capture** + +Example: + +```text +https://webhook.site/your-token +``` + +Pros: +- fastest path when you just need to capture the authorization redirect once +- no local server required +- easy to inspect the query string and copy out `code` and `state` + +Cons: +- not a full OAuth app backend; you still need to exchange the code for tokens yourself +- weaker fit for repeated development flows and team usage +- the authorization code is sensitive, so only use this for short-lived development testing and + avoid shared or long-lived capture URLs + +Practical recommendation: +- use plain `localhost` if you only need a one-off auth code and are fine copying it manually +- use `ngrok` if you are building a real integration or expect to repeat the flow +- use `webhook.site` only for quick one-off testing when you do not yet have a callback handler + +## Step 2: Configure Zoom MCP Scopes + +Add the MCP-specific granular scopes required by the tools you want to use. + +| Product Area | Scope | Zoom label | Needed for | +|--------------|-------|------------|------------| +| AI Companion | `ai_companion:read:search` | Search across Zoom Meeting, Zoom Chat, and Zoom Doc, returning the most relevant results based on the query. | semantic MCP search | +| Meeting | `meeting:read:search` | Search and view meetings | `search_meetings` | +| Meeting | `meeting:read:assets` | View a meeting's assets | `get_meeting_assets` | +| Recording | `cloud_recording:read:list_user_recordings` | Lists all cloud recordings for a user. | `recordings_list` | +| Recording | `cloud_recording:read:content` | read recording content scope | `get_recording_resource` | +| Zoom Docs | `docs:write:import` | Create new file by import | `create_file_with_content` | +| Zoom Docs | `docs:read:export` | Read file content in Markdown format | `get_file_content` | + +Minimum recommendation for the main Zoom MCP connector: +- use a General app +- use user-level OAuth +- include the 7 main MCP scopes above on the same app + +For the dedicated Zoom Docs MCP connector: +- add `docs:write:import` if you want Docs creation +- add `docs:read:export` if you want Docs retrieval +- export the resulting token as `ZOOM_DOCS_MCP_ACCESS_TOKEN` + +Whiteboard MCP uses a separate scope set. See [../whiteboard/SKILL.md](../whiteboard/SKILL.md). + +## Step 3: Authorize and Exchange for Tokens + +1. Construct the authorization URL: + ``` + https://zoom.us/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI + ``` +2. Sign in as the Zoom user who should authorize the app and approve the requested scopes. +3. Copy the `code` from the redirect URL. +4. Exchange the code for tokens: + ```bash + curl -X POST https://zoom.us/oauth/token \ + -u "CLIENT_ID:CLIENT_SECRET" \ + -d "grant_type=authorization_code&code=CODE&redirect_uri=REDIRECT_URI" + ``` +5. Store the returned `access_token` and `refresh_token`. + +Treat the refresh token as **single-use**: +- every successful refresh can return a new refresh token +- persist the newly returned refresh token atomically with the new access token +- stop using the old refresh token after a successful refresh +- avoid concurrent refresh requests against the same stored token + +If you used `webhook.site`, the callback will arrive as a captured request and the `code` +parameter will be in the query string. Exchange it immediately and do not keep using the same +capture URL longer than necessary. + +## Step 4: Enable AI Companion Features + +Smart Recording and Meeting Summary are feature prerequisites for useful semantic meeting +search, meeting assets, and transcript-rich recording content. + +In the Zoom web portal: +1. Go to **Admin → Account Management → Account Settings → AI Companion**. +2. Enable **Smart Recording**. +3. Enable **Meeting Summary**. + +Important: +- these settings do **not** replace the OAuth scopes above +- they affect whether useful recap and transcript content exists for MCP retrieval + +## Step 5: Provide the Token to the Bundled MCP Connector + +Export the token environment variable used by this plugin: + +```bash +export ZOOM_MCP_ACCESS_TOKEN="YOUR_ACCESS_TOKEN" +export ZOOM_DOCS_MCP_ACCESS_TOKEN="YOUR_DOCS_ACCESS_TOKEN" +``` + +Verification: +- restart Claude Code or re-enable the plugin so the bundled MCP server restarts with the token +- confirm the client can see `recordings_list`, `search_meetings`, `get_meeting_assets`, + and `get_recording_resource` +- for the dedicated Docs server, confirm the client can see `create_file_with_content` + and `get_file_content` +- if your client exposes protocol inspection, use `tools/list` as the authority for the live catalog +- run a simple tool such as `recordings_list` to verify the token has the correct MCP scopes + +## Token Lifecycle + +| Property | Details | +|----------|---------| +| Access token expiry | About 1 hour | +| Refresh flow | Exchange `refresh_token` for a new `access_token` and replacement `refresh_token` | +| Client update | Update `ZOOM_MCP_ACCESS_TOKEN`, then restart Claude Code or re-enable the plugin | + +Refresh exchange: + +```bash +curl -X POST https://zoom.us/oauth/token \ + -u "CLIENT_ID:CLIENT_SECRET" \ + -d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN" +``` + +After a successful refresh: +- replace the stored access token +- replace the stored refresh token +- treat the previously used refresh token as spent +- restart Claude Code or re-enable the plugin if you are injecting tokens through env vars + +## Environment Variables + +```bash +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret +ZOOM_MCP_ACCESS_TOKEN=your_access_token +ZOOM_DOCS_MCP_ACCESS_TOKEN=your_docs_access_token +ZOOM_REFRESH_TOKEN=your_refresh_token +``` + +See also: [../../oauth/SKILL.md](../../oauth/SKILL.md) diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/examples/create-zoom-doc.md b/partner-built/zoom-plugin/skills/zoom-mcp/examples/create-zoom-doc.md new file mode 100644 index 00000000..a04a7e91 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/examples/create-zoom-doc.md @@ -0,0 +1,52 @@ +# Create a Zoom Doc + +Use the dedicated `zoom-docs-mcp` server for Zoom Docs creation and retrieval. +The official documented docs tools are `create_file_with_content` and `get_file_content`. + +## Required Scope + +- `docs:write:import` + +## Basic Flow + +### Step 1: Gather the source content + +If the source is meeting content, first use the current Zoom MCP retrieval tools. + +```text +search_meetings + q: "Q1 planning" + from: "2026-03-01" + to: "2026-03-06" +``` + +```text +get_meeting_assets + meetingId: "MEETING_ID_OR_UUID" +``` + +### Step 2: Create the Zoom Doc + +```text +create_file_with_content + file_name: "Q1 Planning — Action Items" + content: "# Action Items\n\n- Owner: ..." +``` + +Optional parameter: +- `parent_id` to place the document under a specific folder or parent object + +## Verified Result Shape + +Successful calls return: +- `file_id` +- `file_link` + +Expect clients to expose at least the created file ID, and in practice often a direct Docs link. +Always parse the actual response you receive from the MCP client. + +## Example Prompts + +- "Create a Zoom Doc with the action items from today's planning meeting." +- "Search for the product review and publish the recap as a Zoom Doc." +- "Turn these Markdown notes into a Zoom Doc." diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/examples/meeting-lifecycle.md b/partner-built/zoom-plugin/skills/zoom-mcp/examples/meeting-lifecycle.md new file mode 100644 index 00000000..88a8787d --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/examples/meeting-lifecycle.md @@ -0,0 +1,50 @@ +# Meeting Lifecycle — Use REST for CRUD + +The current Zoom MCP surface does **not** expose deterministic meeting +create, update, list, or delete tools. + +If the user needs meeting lifecycle management, route to the REST API skill: +- [../../rest-api/SKILL.md](../../rest-api/SKILL.md) + +## What Zoom MCP Is Good For + +Use Zoom MCP for: +- semantic search across meetings +- retrieval of meeting-linked assets +- recording-resource retrieval +- Zoom Docs creation from Markdown + +## What To Use Instead for CRUD + +Use the REST API for: +- create a meeting +- reschedule or update a meeting +- list scheduled meetings deterministically +- delete a meeting or occurrence + +## Hybrid Pattern + +Use MCP first when the user starts with meeting content or recap intent, then hand off to REST +only if the next action becomes a deterministic resource-management step. + +Example: + +```text +1. search_meetings + q: "client onboarding" + from: "2026-03-01" + to: "2026-03-06" + +2. get_meeting_assets + meetingId: "MEETING_ID_OR_UUID" + +3. If the user then wants to reschedule or delete the meeting, + route to zoom-rest-api and perform the CRUD operation there. +``` + +## Example Prompts That Should Route to REST + +- "Create a 45-minute design review for next Tuesday." +- "Move my Q1 planning meeting to Friday at 3pm Pacific." +- "Delete the team sync meeting scheduled for March 10th." +- "Show me all my scheduled meetings for this week." diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/examples/search-and-act.md b/partner-built/zoom-plugin/skills/zoom-mcp/examples/search-and-act.md new file mode 100644 index 00000000..b7675261 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/examples/search-and-act.md @@ -0,0 +1,87 @@ +# Search and Act — Zoom MCP Patterns + +Patterns for finding meetings and taking action using the current Zoom MCP tool surface. + +## Search by Topic or Time Range + +```text +search_meetings + q: "budget review" + from: "2026-03-01" + to: "2026-03-06" + page_size: 10 +``` + +Use this when the user starts from what was discussed rather than from a known meeting ID. + +## Search Then Inspect Assets + +```text +Step 1: +search_meetings + q: "product roadmap" + from: "2026-03-01" + to: "2026-03-06" + +Step 2: +get_meeting_assets + meetingId: "MEETING_ID_OR_UUID" +``` + +This is the main MCP pattern for retrieving summaries, recording references, linked docs, and +other meeting-related assets. + +## Search Then Inspect Recording Resources + +```text +Step 1: +search_meetings + q: "all hands" + from: "2026-03-01" + to: "2026-03-06" + +Step 2: +get_recording_resource + meetingId: "MEETING_ID_OR_UUID" +``` + +Use this when the user needs transcript-capable or playback-oriented resources after they have +identified the target meeting. + +## Search Then Create a Zoom Doc + +```text +Step 1: +search_meetings + q: "Q1 planning" + +Step 2: +get_meeting_assets + meetingId: "MEETING_ID_OR_UUID" + +Step 3: +switch to the dedicated zoom-docs-mcp server + +Step 4: +create_file_with_content + file_name: "Q1 Planning Summary" + content: "# Decisions\n\n- ..." +``` + +## When to Hand Off to REST + +If the user asks for deterministic operations such as: +- create a meeting +- reschedule a meeting +- update meeting settings +- delete a meeting + +route to [../../rest-api/SKILL.md](../../rest-api/SKILL.md) instead of assuming the current +Zoom MCP surface exposes meeting CRUD. + +## Example Prompts + +- "Find meetings about the product launch in February and summarize them." +- "Look up last week's all-hands and pull the recording resources." +- "Search for Q1 planning discussions and create a Zoom Doc from the recap." +- "I need to reschedule a meeting" → hand off to the REST API skill. diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/examples/transcript-retrieval.md b/partner-built/zoom-plugin/skills/zoom-mcp/examples/transcript-retrieval.md new file mode 100644 index 00000000..500bfd1f --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/examples/transcript-retrieval.md @@ -0,0 +1,106 @@ +# Transcript and Summary Retrieval + +Use the Zoom MCP server when the goal is to search meeting content, retrieve meeting-linked +assets, or fetch recording-oriented resources after a meeting. + +## Path 1: Semantic Search and Meeting Assets + +Use this path when the user starts from content, topic, or time range. + +### Prerequisites + +- `meeting:read:search` +- `meeting:read:assets` +- AI Companion features such as **Smart Recording** and **Meeting Summary** enabled for useful recap data + +### Step 1: Search meetings + +```text +search_meetings + q: "Q4 budget discussion" + from: "2026-03-01" + to: "2026-03-06" + page_size: 20 +``` + +### Step 2: Choose a meeting and retrieve assets + +```text +get_meeting_assets + meetingId: "MEETING_ID_OR_UUID" +``` + +Expect the result to contain a meeting-linked asset bundle. In practice, this can include: +- AI summary or recap-linked content +- recording references +- linked documents +- whiteboard links + +Do not hardcode exact field names without checking a current live response. + +## Path 2: Recording-Oriented Retrieval + +Use this path when the user needs transcript-capable resources, recording playback resources, +or recording lookup by user/date range. + +### Prerequisites + +- `cloud_recording:read:list_user_recordings` +- `cloud_recording:read:content` + +### Step 1: List recordings + +```text +recordings_list + userId: "me" + from: "2026-03-01" + to: "2026-03-06" + page_size: 50 +``` + +### Step 2: Retrieve recording resources + +```text +get_recording_resource + meetingId: "MEETING_UUID_OR_RECORDING_ID" +``` + +Use this when the user needs recording-oriented data such as: +- transcript timelines +- summary segments +- next-step segments +- playback URLs +- recording metadata + +## Choosing Between the Paths + +| Need | Preferred path | +|------|----------------| +| Search by topic or what was discussed | `search_meetings` → `get_meeting_assets` | +| Inspect all meeting-linked assets | `get_meeting_assets` | +| List recordings by user/date | `recordings_list` | +| Fetch recording-oriented resources for a specific meeting | `get_recording_resource` | + +## Direct Downloads + +If a returned recording resource includes a direct URL, fetch it with the same bearer token. +The URL is not public by default. + +## Troubleshooting + +**`search_meetings` fails with missing scope:** +- add `meeting:read:search` + +**`get_meeting_assets` fails with missing scope:** +- add `meeting:read:assets` + +**`recordings_list` fails with missing scope:** +- add `cloud_recording:read:list_user_recordings` + +**`get_recording_resource` fails with missing scope:** +- add `cloud_recording:read:content` + +**Search results are sparse or low quality:** +- enable Smart Recording and Meeting Summary +- widen the date window +- fall back to `recordings_list` if the user is really asking for post-meeting recording content diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/references/error-codes.md b/partner-built/zoom-plugin/skills/zoom-mcp/references/error-codes.md new file mode 100644 index 00000000..caf83dd3 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/references/error-codes.md @@ -0,0 +1,59 @@ +# Error Codes — Zoom MCP Server + +Errors can surface from the MCP protocol layer or from the underlying Zoom API-style behavior +behind a tool call. + +## MCP Protocol Errors + +| Code | Message | Cause | Fix | +|------|---------|-------|-----| +| `-32001` | `Access token is required` | Missing Authorization header because the bundled connector has no token | Set `ZOOM_MCP_ACCESS_TOKEN`, then restart Claude Code or re-enable the plugin | +| `-32001` | `Invalid access token` | Expired, revoked, malformed, or missing required scopes | Refresh the token and verify the MCP-specific scopes | +| `-32602` | `Can not found tool: ... in this MCP Server` | Wrong tool name or wrong MCP server surface | Re-run `tools/list` and use the current live tool names for that endpoint | +| `-32603` | `Call handle error` | Missing required parameter or server-side call handling failure | Re-check required arguments against the live schema and retry | + +## Exact Scope Errors Observed in Testing + +| Tool | Exact missing-scope error | +|------|---------------------------| +| `search_meetings` | `meeting:read:search` | +| `get_meeting_assets` | `meeting:read:assets` or `meeting:read:assets:admin` | +| `recordings_list` | `cloud_recording:read:list_user_recordings` or admin/master variants | +| `get_recording_resource` | `cloud_recording:read:content` | +| `create_file_with_content` | `docs:write:import` worked with user OAuth; S2S runtime error surfaced `docs_import:write` aliasing | +| `get_file_content` | `docs:read:export` | + +## Recording and Transcript Failures + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| `get_recording_resource` fails on scope | Missing `cloud_recording:read:content` | Add the scope and mint a new token | +| Recording search/list works but transcript-capable retrieval fails | Token has list scope but not content scope | Add `cloud_recording:read:content` | +| Returned resource URL gives `401` | Bearer token not included on the follow-up fetch | Include the same bearer token on the direct request | + +## Upstream API Validation Errors + +Some tool calls can succeed at the MCP protocol layer but surface a downstream Zoom API +validation error in the result payload. + +Common downstream validation response: + +| Tool | Error shape | Cause | Fix | +|------|-------------|-------|-----| +| `create_file_with_content` | `Upstream API returned error status code: 400 ... "message":"invalid param"` | Invalid parameter value such as a bogus `parent_id` | Fix the argument value and retry | + +## Whiteboard Server Split + +The Zoom MCP server (`mcp-us.zoom.us/mcp/zoom/streamable`), the Zoom Docs MCP server +(`mcp.zoom.us/mcp/docs/streamable`), and the Whiteboard MCP server +(`mcp-us.zoom.us/mcp/whiteboard/streamable`) are separate surfaces. + +Use the dedicated Whiteboard MCP skill for Whiteboard-specific auth, scopes, and identifier mapping: +- [../whiteboard/SKILL.md](../whiteboard/SKILL.md) + +## Debugging Checklist + +1. Confirm the client can discover the current MCP tool list through `tools/list`. +2. Confirm the bearer token is fresh. +3. Verify the exact MCP-specific scopes, not just adjacent REST scopes. +4. Re-discover the target meeting or recording instead of reusing guessed IDs. diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/references/tools.md b/partner-built/zoom-plugin/skills/zoom-mcp/references/tools.md new file mode 100644 index 00000000..33d5414f --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/references/tools.md @@ -0,0 +1,149 @@ +# Tool Reference — Zoom MCP Servers + +Tools available on the bundled Zoom MCP servers. Treat the raw server tool names as authoritative. +Some MCP clients namespace them in the UI, for example `zoom-mcp:recordings_list` or +`zoom-docs-mcp:create_file_with_content`. + +This reference is based on the current documented and observed tool surfaces of: +- `https://mcp-us.zoom.us/mcp/zoom/streamable` +- `https://mcp.zoom.us/mcp/docs/streamable` + +Treat the live MCP protocol `tools/list` response as the authoritative source for the current +tool list and schemas. + +## Main Zoom MCP Server Tools + +The main Zoom MCP server exposes these tools: + +- `get_meeting_assets` +- `search_meetings` +- `get_recording_resource` +- `recordings_list` + +The server did **not** expose older inferred tool names such as `list_meetings`, +`get_meeting`, `create_meeting`, `get_user_profile`, `list_available_tools`, or +`get_tool_details` in that probe. + +## Supported Scope Families Advertised by Zoom MCP + +Protected-resource metadata for the main Zoom MCP server advertised these scope families: +- `ai_companion:read:search` +- `meeting:read:assets` +- `meeting:read:search` +- `cloud_recording:read:content` +- `cloud_recording:read:list_user_recordings` +- `docs:write:import` +- `docs:read:export` + +## Zoom Docs MCP Server Tools + +The dedicated Zoom Docs MCP server exposes these documented tools: + +- `create_file_with_content` +- `get_file_content` + +### `create_file_with_content` + +Create a Zoom Docs document from Markdown content. + +**Verified scope:** `docs:write:import` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `content` | string | **Yes** | Markdown formatted content | +| `file_name` | string | No | Name of the new document | +| `parent_id` | string | No | Parent file/folder ID; omit to place under My Docs | + +Successful calls return: +- `file_id` +- `file_link` + +### `get_file_content` + +Retrieve a Zoom Docs document in Markdown format. + +**Verified scope:** `docs:read:export` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `fileId` | string | **Yes** | The unique identifier of the document file | + +## Meeting Discovery and Assets + +### `search_meetings` + +Read-only search tool for semantic meeting discovery. + +**Verified scope:** `meeting:read:search` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `q` | string | No | Search query keyword | +| `from` | string | No | UTC start datetime | +| `to` | string | No | UTC end datetime | +| `page_size` | integer | No | Results per page; default `50`, max `300` | +| `next_page_token` | string | No | Pagination token; expires in 15 minutes | + +### `get_meeting_assets` + +Read-only meeting asset hub. Retrieves meeting summary, recording, whiteboards, Zoom Docs, +and related artifacts for a specific meeting. + +**Verified scope:** `meeting:read:assets` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `meetingId` | string | **Yes** | Numeric meeting number or UUID; the schema prefers UUID when available | + +**Important live-schema note:** +- UUID-style values may require double encoding when they contain `/` or `//`. +- The tool description strongly prefers explicit user selection when choosing a meeting from search results. + +## Recordings + +### `recordings_list` + +List cloud recordings for a user. + +**Verified scope:** `cloud_recording:read:list_user_recordings` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `userId` | string | **Yes** | User ID; user OAuth commonly accepts `me` | +| `from` | string | No | Start date | +| `to` | string | No | End date | +| `meeting_id` | integer | No | Filter by meeting number | +| `trash` | boolean | No | Include trashed recordings | +| `trash_type` | string | No | Trash filter category | +| `mc` | string | No | Additional recording filter flag from the live schema | +| `page_size` | integer | No | Results per page; default `30`, max `300` | +| `next_page_token` | string | No | Pagination token | + +### `get_recording_resource` + +Retrieve recording-oriented assets for a specific meeting, including transcript-like, +summary-like, and playback-oriented resources. + +**Verified scope:** `cloud_recording:read:content` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `meetingId` | string | **Yes** | Meeting UUID or recording-capable identifier | +| `types` | string | No | Resource type selector | +| `clip_num` | integer | No | Clip number / segment selector | +| `play_time` | integer | No | Playback position | +| `raw_passcode` | string | No | Plaintext recording passcode | +| `encode_passcode` | string | No | Encoded recording passcode | + +**Output shape from live schema includes resource families such as:** +- transcript timelines +- summaries +- next steps +- play URLs + +## Discovery Notes + +- Discovery happens through MCP protocol `tools/list`, not through a dedicated Zoom utility tool. +- Re-run `tools/list` whenever you need to confirm whether the current tool list has changed. +- Do not rely on older examples that use `query`, `startDate`, `endDate`, or `pageSize`; the current live schema uses `q`, `from`, `to`, and `page_size`. +- Do not rely on older examples that route Docs creation through the main `zoom-mcp` server; Zoom Docs now have a dedicated `zoom-docs-mcp` server in this plugin. diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/troubleshooting/common-errors.md b/partner-built/zoom-plugin/skills/zoom-mcp/troubleshooting/common-errors.md new file mode 100644 index 00000000..c7f04acb --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/troubleshooting/common-errors.md @@ -0,0 +1,130 @@ +# Common Errors — Zoom MCP Server + +Diagnostic guide for the most frequent issues when using the Zoom MCP server. + +## Authentication Issues + +### `Access token is required` (`-32001`) + +**Cause:** The bundled connector does not have a token available, so no Authorization header is +sent to the Zoom MCP endpoint. + +**Fix:** Set `ZOOM_MCP_ACCESS_TOKEN` and restart Claude Code or re-enable the plugin. + +```bash +export ZOOM_MCP_ACCESS_TOKEN="YOUR_ACCESS_TOKEN" +``` + +### `Invalid access token` (`-32001`) + +**Cause:** The access token expired, was revoked, or does not contain the exact MCP scopes needed +for the tool you called. + +**Fix:** Refresh the token, update `ZOOM_MCP_ACCESS_TOKEN`, restart Claude Code, and verify the MCP-specific granular scopes. + +## Scope Issues + +### `search_meetings` fails on scope + +**Server-required scope:** `meeting:read:search` + +### `get_meeting_assets` fails on scope + +**Server-required scope:** `meeting:read:assets` + +### `recordings_list` fails on scope + +**Server-required scope:** `cloud_recording:read:list_user_recordings` + +### `get_recording_resource` fails on scope + +**Server-required scope:** `cloud_recording:read:content` + +### `create_file_with_content` fails on scope + +**Required scope:** `docs:write:import` + +### `get_file_content` fails on scope + +**Required scope:** `docs:read:export` + +## Search and Retrieval Issues + +### Semantic search returns no useful results + +Common causes: +- AI Companion features were not enabled for those meetings +- the query is too narrow +- the date window is too narrow +- the meeting is easier to locate through `recordings_list` + +**Fixes:** +- enable Smart Recording and Meeting Summary +- widen `from` and `to` +- try shorter search terms +- fall back to `recordings_list` + +### Meeting assets retrieval fails even with a valid token + +Most often this means one of these: +- missing `meeting:read:assets` +- guessed `meetingId` instead of the right UUID or numeric ID +- user does not have access to that meeting's assets + +### Recording-oriented retrieval fails + +If `get_recording_resource` cannot find the target: +- use `recordings_list` first +- use the identifier returned by the live listing flow +- avoid assuming a scheduled meeting number is the same identifier used by the recording flow + +### Direct resource download returns `401` + +Returned resource URLs still require the bearer token. + +**Fix:** Include `Authorization: Bearer YOUR_TOKEN` when fetching the URL directly. + +## Connection Issues + +### `Can not found tool: ... in this MCP Server` (`-32602`) + +**Cause:** The requested tool does not exist on the active MCP server, or the request was +sent to the wrong MCP surface. + +**Fix:** +- Zoom MCP: `https://mcp-us.zoom.us/mcp/zoom/streamable` +- Zoom Docs MCP: `https://mcp.zoom.us/mcp/docs/streamable` +- Whiteboard MCP: `https://mcp-us.zoom.us/mcp/whiteboard/streamable` +- re-run `tools/list` +- use the current tool names exposed by that server +- if the request is Zoom Docs-specific, use the dedicated Docs MCP server +- if the request is Whiteboard-specific, route to [../whiteboard/SKILL.md](../whiteboard/SKILL.md) + +### MCP server not appearing in the client + +**Fix:** +- confirm the plugin is enabled +- confirm `ZOOM_MCP_ACCESS_TOKEN` is set +- restart Claude Code so the bundled MCP server restarts + +## Parameter and Call-Handling Issues + +### `Call handle error` (`-32603`) + +**Cause:** The tool call was accepted at the protocol layer, but the server failed while +handling the call. One observed case was calling `recordings_list` without the +required arguments. + +**Fix:** +- re-check required parameters against the live schema from `tools/list` +- retry with the missing arguments added + +### Upstream `400 invalid param` + +**Cause:** The MCP tool reached the downstream Zoom API, but one of the passed arguments was +invalid. One observed case was calling `create_file_with_content` with a +bogus `parent_id`. + +**Fix:** +- remove the bad argument or replace it with a valid value +- retry the tool call diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/SKILL.md b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/SKILL.md new file mode 100644 index 00000000..a53ed278 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/SKILL.md @@ -0,0 +1,97 @@ +--- +name: zoom-mcp/whiteboard +description: | + Guidance for the bundled Zoom Whiteboard MCP connector. Use for Whiteboard MCP auth, + endpoints, ID mapping, and tool workflows such as list_whiteboards and get_a_whiteboard. + Prefer this skill when the request is specifically about Whiteboard MCP rather than general Zoom MCP. +user-invocable: false +triggers: + - "whiteboard mcp" + - "zoom whiteboard mcp" + - "zoom mcp whiteboard" + - "zoom whiteboard tools" + - "list zoom whiteboards" + - "get zoom whiteboard" + - "zoom whiteboard id" + - "zoom wb/db" +--- + +# Zoom MCP Whiteboard + +Dedicated guidance for Zoom's Whiteboard MCP server. + +## Endpoints + +| Transport | URL | +|-----------|-----| +| Streamable HTTP (recommended) | `https://mcp-us.zoom.us/mcp/whiteboard/streamable` | +| SSE (fallback) | `https://mcp-us.zoom.us/mcp/whiteboard/sse` | + +## Authentication + +- **User OAuth with Whiteboard scopes** is the verified working path for `list_whiteboards` + and `get_a_whiteboard`. +- **S2S OAuth** can reach the Whiteboard MCP gateway and complete `tools/list`, but tool + execution must be validated separately for your app and Whiteboard scopes. +- Practical rule: start with **user OAuth** for Whiteboard MCP unless you have already + proven your S2S app can mint and execute with the required Whiteboard scopes. +- The bundled connector expects the token in `ZOOM_WHITEBOARD_MCP_ACCESS_TOKEN`. + +Reference: [references/authentication-and-identifiers.md](references/authentication-and-identifiers.md) + +## Required Scopes + +Whiteboard MCP read scopes: +- `whiteboard:read:list_whiteboards` +- `whiteboard:read:whiteboard` + +Write-capable Whiteboard metadata advertised by the gateway: +- `whiteboard:write:whiteboard` + +## Whiteboard ID Mapping + +For Whiteboard MCP, use the identifier from the URL segment after `/wb/db/`, not the numeric +segment after `/p/`. + +Example: + +```text +https://us05whiteboard.zoom.us/wb/db/6iktP8hJT3e5qaCuwFuAGg/p/180968285929472 + ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ + whiteboard_id page/subresource id +``` + +## Available Tools + +The current Whiteboard MCP tool surface is: + +- `create_a_whiteboard_for_brainstorming` +- `list_whiteboards` +- `create_a_whiteboard` +- `get_a_whiteboard` +- `create_a_whiteboard_by_script` +- `update_a_whiteboard_metadata` +- `create_a_whiteboard_for_meeting_summary` +- `create_a_whiteboard_for_strategy_analysis` + +Some MCP clients namespace server tools in the UI. Treat the raw tool names above as +authoritative. + +Reference: [references/tools.md](references/tools.md) + +## Read Workflow + +1. Use a user OAuth token with the Whiteboard read scopes. +2. Call `list_whiteboards` to discover accessible whiteboards and confirm the correct `whiteboard_id`. +3. Call `get_a_whiteboard` with that `whiteboard_id`. + +## Chaining + +- Parent MCP skill: [../SKILL.md](../SKILL.md) +- OAuth guidance: [../concepts/oauth-setup.md](../concepts/oauth-setup.md) +- General routing: [../../general/SKILL.md](../../general/SKILL.md) + +## References + +- [references/authentication-and-identifiers.md](references/authentication-and-identifiers.md) - Auth behavior and Whiteboard ID mapping. +- [references/tools.md](references/tools.md) - Whiteboard MCP tool catalog. diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/authentication-and-identifiers.md b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/authentication-and-identifiers.md new file mode 100644 index 00000000..e2c1b7e6 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/authentication-and-identifiers.md @@ -0,0 +1,56 @@ +# Authentication and Identifiers — Whiteboard MCP + +Guidance for `https://mcp-us.zoom.us/mcp/whiteboard/streamable` and +`https://mcp-us.zoom.us/mcp/whiteboard/sse`. + +## Authentication Behavior + +### User OAuth + +User OAuth with Whiteboard scopes was the verified working path for: + +- `list_whiteboards` +- `get_a_whiteboard` + +The required user OAuth scope set includes: +- `whiteboard:read:list_whiteboards` +- `whiteboard:read:whiteboard` + +### Server-to-Server OAuth + +S2S OAuth can reach the Whiteboard MCP gateway and complete protocol discovery through +`tools/list`, but Whiteboard read-tool execution must be validated separately for your app. + +One possible runtime error shape when S2S lacks the expected scopes is: + +```text +Invalid access token, does not contain scopes:[whiteboard:read:admin,whiteboard:read] +``` + +Protected-resource metadata for Whiteboard MCP advertised these supported scopes: +- `whiteboard:write:whiteboard` +- `whiteboard:read:list_whiteboards` +- `whiteboard:read:whiteboard` + +Practical guidance: +- Use user OAuth first for Whiteboard MCP. +- Only rely on S2S after you have confirmed the app can mint and execute with the required Whiteboard scopes. + +## Whiteboard ID Mapping + +The MCP-facing `whiteboard_id` matches the URL segment after `/wb/db/`. + +Example: + +```text +https://us05whiteboard.zoom.us/wb/db/6iktP8hJT3e5qaCuwFuAGg/p/180968285929472 +``` + +- Correct MCP `whiteboard_id`: `6iktP8hJT3e5qaCuwFuAGg` +- The numeric `/p/...` segment is not the Whiteboard MCP identifier. + +## Response Shape + +`get_a_whiteboard` returned a list-style payload containing the matched whiteboard rather than +an obviously single-object payload. Parse the response shape you actually receive from the live +server instead of assuming the tool name implies a unique-object response. diff --git a/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/tools.md b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/tools.md new file mode 100644 index 00000000..32ad2395 --- /dev/null +++ b/partner-built/zoom-plugin/skills/zoom-mcp/whiteboard/references/tools.md @@ -0,0 +1,23 @@ +# Tools — Whiteboard MCP + +Current `tools/list` result from `https://mcp-us.zoom.us/mcp/whiteboard/streamable`. + +## Tool Catalog + +| Tool | Purpose | Guide coverage | +|------|---------|----------------| +| `create_a_whiteboard_for_brainstorming` | Create a whiteboard from brainstorm inputs such as problems, ideas, and next steps | Available on the Whiteboard MCP surface; validate request schema before use | +| `list_whiteboards` | List accessible whiteboards for the current user or admin context | Covered by the read workflow in this guide | +| `create_a_whiteboard` | Create a whiteboard | Available on the Whiteboard MCP surface; validate request schema before use | +| `get_a_whiteboard` | Retrieve a whiteboard by `whiteboard_id` | Covered by the read workflow in this guide | +| `create_a_whiteboard_by_script` | Create a whiteboard using scripted content or structured input | Available on the Whiteboard MCP surface; validate request schema before use | +| `update_a_whiteboard_metadata` | Update whiteboard metadata | Available on the Whiteboard MCP surface; validate request schema before use | +| `create_a_whiteboard_for_meeting_summary` | Generate a whiteboard from meeting-summary style input | Available on the Whiteboard MCP surface; validate request schema before use | +| `create_a_whiteboard_for_strategy_analysis` | Generate a whiteboard for strategy-analysis content | Available on the Whiteboard MCP surface; validate request schema before use | + +## Supported Scope Families Advertised by Whiteboard MCP + +Protected-resource metadata for Whiteboard MCP advertised: +- `whiteboard:write:whiteboard` +- `whiteboard:read:list_whiteboards` +- `whiteboard:read:whiteboard`