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
+ Loading...
+
+
+ SSN:
+ Credit Card:
+
+
+
+
+```
+
+### 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?
+ Loading...
+
+
+
+
+
+```
+
+## 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
+
+
+
+
+Start Support Session
+```
+
+## 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
+
+
+
+
+
+
+
+```
+
+## 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
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## 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 (
+ Join Meeting
+ );
+}
+```
+
+### 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 (
+
+
+
+ {isJoining ? 'Joining...' : 'Join Meeting'}
+
+ {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 && (
+
Join Meeting
+ )}
+ {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