Zoom Oauth
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to implementing Zoom OAuth, covering various app types including user-level, account-level, and server-to-server OAuth. It assists developers in selecting the correct grant flow, managing token lifecycles, and diagnosing common authentication errors. The skill emphasizes secure practices, such as handling refresh tokens as single-use values and maintaining proper scope architecture.
What it does
- Guides selection of appropriate OAuth grant flows like PKCE or authorization code.
- Provides workflows for identifying app types and required scopes.
- Offers best practices for secure token management and refresh token rotation.
- Assists in troubleshooting common OAuth errors and redirect URI issues.
- Clarifies the distinction between classic and granular scope usage.
- Defines standard environment variables required for Zoom API integration.
When to use it
- When setting up authentication for a new Zoom Marketplace application.
- When debugging failed authorization requests or token refresh issues.
- When determining the correct OAuth scopes for specific API endpoints.
- When transitioning from classic scopes to granular scopes.
- When implementing server-to-server OAuth for automated account-level tasks.
Advantages
- Reduces integration errors by providing clear flow selection criteria.
- Promotes security by enforcing single-use refresh token patterns.
- Simplifies scope management through structured lookup workflows.
- Centralizes configuration requirements and environment variable standards.
Usage examples
- “How do I implement authorization code flow with PKCE for my public Zoom app?”
- The agent will outline the steps to generate a code challenge, initiate the authorization request, and exchange the code for an access token while ensuring PKCE security.
- “I am getting an invalid scope error when calling the meeting API. How do I fix this?”
- The agent will guide you to verify your app's configured scopes against the endpoint requirements and check if you are using deprecated classic scopes instead of granular ones.
- “What is the correct way to handle Zoom refresh tokens?”
- The agent will explain the requirement to persist the new refresh token returned by every refresh response and discard the old one to maintain security.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/openai/zoom-oauth/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip zoom-oauth.zip -d .agents/skills
Documentation by Zoom
Zoom OAuth
Use this skill for concrete Zoom authentication implementation and troubleshooting. Prefer setup-zoom-oauth for the first-pass setup plan, then return here for exact flows, scope behavior, refresh handling, and error diagnosis.
Workflow
Identify the app type and actor: user-level OAuth, account-level OAuth, server-to-server OAuth where officially supported, SDK JWT, or Build-platform credentials.
Confirm the target API, SDK, or app surface, because scopes and token audiences differ by surface.
Choose the grant flow: authorization code with PKCE for public clients, authorization code for confidential web apps, device authorization where appropriate, or account credentials for supported account-level automation.
Store refresh tokens as single-use values: persist the replacement refresh token returned by each refresh response before reusing the old one.
Validate requests against redirect URI, account ID, scopes, app publication state, and token expiration before changing application code.
For local development, keep access tokens out of logs and treat refresh tokens as single-use when rotating credentials.
References
Full preserved guide: references/full-guide.md
OAuth flows: concepts/oauth-flows.md
Token lifecycle: concepts/token-lifecycle.md
Scope architecture: concepts/scopes-architecture.md
Granular scopes: references/granular-scopes.md
OAuth errors: references/oauth-errors.md
Redirect URI issues: troubleshooting/redirect-uri-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer862 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md3.3 KiB
- SKILL.md1.8 KiB
- agents/openai.yaml131 B
- concepts/oauth-flows.md22.2 KiB
- concepts/pkce.md13.8 KiB
- concepts/scopes-architecture.md6.7 KiB
- concepts/state-parameter.md8.1 KiB
- concepts/token-lifecycle.md15.1 KiB
- examples/device-flow.md371 B
- examples/pkce-implementation.md379 B
- examples/s2s-oauth-basic.md375 B
- examples/s2s-oauth-redis.md6.6 KiB
- examples/token-refresh.md373 B
- examples/user-oauth-basic.md376 B
- examples/user-oauth-mysql.md376 B
- references/classic-scopes.md2.5 KiB
- references/environment-variables.md863 B
- references/full-guide.md31.7 KiB
- references/granular-scopes.md1.8 KiB
- references/oauth-errors.md4.8 KiB
- troubleshooting/common-errors.md458 B
- troubleshooting/redirect-uri-issues.md229 B
- troubleshooting/scope-issues.md222 B
- troubleshooting/token-issues.md222 B
concepts/oauth-flows.md
Zoom OAuth Flows
Zoom supports 4 OAuth 2.0 flows. This guide helps you choose the right one and understand how each works.
Endpoint split to remember:
Authorization URL:
https://zoom.us/oauth/authorizeToken URL:
https://zoom.us/oauth/token
Quick Decision Matrix
| Your Scenario | Flow | Grant Type |
|---|---|---|
| Backend automation on your own account | S2S OAuth | account_credentials |
| SaaS app for other Zoom users | User OAuth | authorization_code |
| Device without browser (TV, kiosk, IoT) | Device Flow | urn:ietf:params:oauth:grant-type:device_code |
| Team Chat bot only | Chatbot | client_credentials |
Two-Legged vs Three-Legged
| Type | User Involved? | Zoom Flows |
|---|---|---|
| Two-legged | No (app acts on its own) | S2S OAuth, Chatbot |
| Three-legged | Yes (user authorizes app) | User OAuth, Device Flow |
1. Server-to-Server (S2S) OAuth
When to use:
Backend automation on your own Zoom account
No end-user interaction needed
Account-wide API access
Grant type: account_credentials
Token lifetime:
Access token: 1 hour
Refresh token: None (request new token when expired)
Credentials required:
Account ID
Client ID
Client Secret
Flow Diagram
┌──────────────┐ ┌──────────────┐
│ Your App │ │ Zoom OAuth │
│ (Backend) │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
│ POST /oauth/token │
│ grant_type=account_credentials │
│ account_id={ACCOUNT_ID} │
│ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │
│──────────────────────────────────────────────────>│
│ │
│ │ Validate
│ │ credentials
│ │
│ { access_token, expires_in, scope } │
│<──────────────────────────────────────────────────│
│ │
│ API Requests with Bearer token │
│ (valid for 1 hour) │
│ │Implementation
const axios = require('axios');
const qs = require('query-string');
const getToken = async () => {
const response = await axios.post(
'https://zoom.us/oauth/token',
qs.stringify({
grant_type: 'account_credentials',
account_id: process.env.ZOOM_ACCOUNT_ID
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data; // { access_token, expires_in, scope, token_type }
};Key Points
✅ Simple: No redirect URIs, no user interaction ✅ Secure: Credentials stored server-side only ✅ Account-wide: Single token for all account operations ⚠️ No refresh token: Just request a new token when expired (cache with TTL)
2. User Authorization OAuth
When to use:
Building a SaaS app for other Zoom users
Users authorize your app to act on their behalf
Need per-user access control
Grant type: authorization_code
Token lifetime:
Access token: 1 hour
Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior)
Credentials required:
Client ID
Client Secret
Redirect URI (must match marketplace app config)
Flow Diagram
┌────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User │ │ Your App │ │ Zoom OAuth │ │ Zoom API │
│Browser │ │ (Server) │ │ Server │ │ Server │
└────┬───┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
│ 1. Click "Add App" │ │ │
│─────────────────────>│ │ │
│ │ │ │
│ 2. Redirect to authorize │ │
│https://zoom.us/oauth/authorize? │ │
│ client_id={ID} │ │
│ redirect_uri={URI} │ │
│ response_type=code │ │
│ state={RANDOM} │ │
│<─────────────────────│ │ │
│ │ │ │
│ 3. User sees "Allow" page │ │
│─────────────────────────────────────────────────>│ │
│ │ │ │
│ 4. User clicks "Allow" │ │
│─────────────────────────────────────────────────>│ │
│ │ │ │
│ 5. Redirect to callback │ │
│ {REDIRECT_URI}?code={CODE}&state={STATE} │ │
│<─────────────────────────────────────────────────│ │
│ │ │ │
│ 6. Send code to app │ │ │
│─────────────────────>│ │ │
│ │ │ │
│ │ 7. Exchange code for token │
│ │ POST /oauth/token │
│ │ grant_type=authorization_code │
│ │ code={CODE} │
│ │ redirect_uri={URI} │
│ │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │
│ │─────────────────────────────────────────────────────>│
│ │ │ │
│ │ 8. Return tokens │ │
│ │ { access_token, refresh_token, expires_in } │
│ │<─────────────────────────────────────────────────────│
│ │ │ │
│ │ 9. Store tokens (encrypted) │
│ │ per user │ │
│ │ │ │
│ │ 10. API requests │
│ │ Authorization: Bearer {ACCESS_TOKEN} │
│ │─────────────────────────────────────────────────────────────────>│Implementation
Step 1: Redirect to Authorization
const express = require('express');
const crypto = require('crypto');
app.get('/auth', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state; // Store for verification
const authURL = new URL('https://zoom.us/oauth/authorize');
authURL.searchParams.set('response_type', 'code');
authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID);
authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL);
authURL.searchParams.set('state', state);
res.redirect(authURL.toString());
});Step 2: Handle Callback and Exchange Code
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state parameter');
}
try {
const response = await axios.post(
'https://zoom.us/oauth/token',
qs.stringify({
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.ZOOM_REDIRECT_URL
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const { access_token, refresh_token } = response.data;
// Store tokens securely (encrypted) per user
await saveUserTokens(req.session.userId, {
access_token,
refresh_token
});
res.send('Authorization successful!');
} catch (error) {
res.status(500).send('Token exchange failed');
}
});Key Points
✅ User-controlled: Users authorize access to their own account ✅ Per-user tokens: Each user gets their own access/refresh tokens ✅ Refresh support: Tokens can be refreshed while the refresh token remains valid (lifetime varies; ~90 days is common) ⚠️ Redirect URI must match exactly: Including trailing slash, protocol, port ⚠️ State parameter required: Prevent CSRF attacks ⚠️ Authorization code expires in 5 minutes: Exchange immediately
3. Device Authorization Flow
When to use:
Devices without a browser (smart TVs, kiosks, IoT devices)
Devices with limited input capabilities
User authorizes on a separate device (phone/computer)
Grant type: urn:ietf:params:oauth:grant-type:device_code
Token lifetime:
Access token: 1 hour
Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior)
Credentials required:
Client ID
Client Secret
Flow Diagram
┌────────────┐ ┌──────────────┐ ┌────────────┐
│ Device │ │ Zoom OAuth │ │User's Phone│
│ (TV/Kiosk) │ │ Server │ │ / Computer │
└──────┬─────┘ └──────┬───────┘ └─────┬──────┘
│ │ │
│ 1. POST /oauth/devicecode │
│ client_id={CLIENT_ID} │
│───────────────────────>│ │
│ │ │
│ 2. Return device_code, user_code, verification_uri, interval
│ { device_code, user_code, verification_uri, interval }
│<───────────────────────│ │
│ │ │
│ 3. Display to user: │ │
│ "Go to zoom.us/activate" │
│ "Enter code: ABC-DEF" │ │
│ │ │
│ │ 4. User visits URL │
│ │ and enters user_code │
│ │<────────────────────────│
│ │ │
│ │ 5. User clicks "Allow" │
│ │<────────────────────────│
│ │ │
│ 6. Poll for token (every {interval} seconds) │
│ POST /oauth/token │
│ grant_type=urn:ietf:params:oauth:grant-type:device_code
│ device_code={DEVICE_CODE} │
│───────────────────────>│ │
│ │ │
│ 7. Response (repeat until success or timeout) │
│ - authorization_pending (keep polling) │
│ - slow_down (increase interval) │
│ - expired_token (restart flow) │
│ - { access_token, refresh_token } (success!) │
│<───────────────────────│ │Implementation
Step 1: Request Device Code
const requestDeviceCode = async () => {
const response = await axios.post(
'https://zoom.us/oauth/devicecode',
qs.stringify({
client_id: process.env.ZOOM_CLIENT_ID
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data;
/*
{
device_code: "GmRhmhcxhwAzkoEqiMEg_DnyEysNmsh6JCl-fNkAghaUg",
user_code: "ABC-DEF",
verification_uri: "https://zoom.us/activate",
expires_in: 900, // 15 minutes
interval: 5 // Poll every 5 seconds
}
*/
};Step 2: Display User Code
const { device_code, user_code, verification_uri, interval } = await requestDeviceCode();
console.log(`\nGo to: ${verification_uri}`);
console.log(`Enter code: ${user_code}\n`);Step 3: Poll for Token
const pollForToken = async (device_code, interval) => {
const pollInterval = interval * 1000; // Convert to milliseconds
let currentInterval = pollInterval;
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const response = await axios.post(
'https://zoom.us/oauth/token',
qs.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: device_code
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
// Success! Got tokens
resolve(response.data);
} catch (error) {
const errorCode = error.response?.data?.error;
if (errorCode === 'authorization_pending') {
// User hasn't authorized yet, keep polling
setTimeout(poll, currentInterval);
} else if (errorCode === 'slow_down') {
// Zoom wants us to slow down, increase interval by 5s
currentInterval += 5000;
setTimeout(poll, currentInterval);
} else if (errorCode === 'expired_token') {
// Device code expired (15 minutes), restart flow
reject(new Error('Device code expired. Please restart authorization.'));
} else {
// Other error
reject(error);
}
}
};
// Start polling
poll();
});
};Key Points
✅ No browser required: User authorizes on separate device
✅ Simple user experience: Just enter a short code
✅ Polling-based: Device polls until user authorizes
⚠️ Must enable in app settings: "Use App on Device" feature flag
⚠️ Device code expires in 15 minutes: User must complete authorization quickly
⚠️ Respect polling interval: Returned by /devicecode endpoint (usually 5s)
⚠️ Handle slow_down: Increase interval by 5s when requested
4. Client Authorization (Chatbot)
When to use:
Building a Team Chat bot ONLY
App needs
imchat:botscopeSimpler than S2S OAuth
Grant type: client_credentials
Token lifetime:
Access token: 1 hour
Refresh token: None (request new token when expired)
Credentials required:
Client ID
Client Secret
Flow Diagram
┌──────────────┐ ┌──────────────┐
│ Chatbot App │ │ Zoom OAuth │
│ (Backend) │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
│ POST /oauth/token │
│ grant_type=client_credentials │
│ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │
│──────────────────────────────────────────────────>│
│ │
│ { access_token, expires_in, scope } │
│<──────────────────────────────────────────────────│
│ │
│ Chatbot API Requests with Bearer token │
│ (valid for 1 hour) │
│ │Implementation
const getChatbotToken = async () => {
const response = await axios.post(
'https://zoom.us/oauth/token',
qs.stringify({
grant_type: 'client_credentials'
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data; // { access_token, expires_in, scope, token_type }
};Key Points
✅ Simplest flow: Just request token with credentials
✅ Chatbot-specific: Limited to Team Chat bot operations
⚠️ No refresh token: Request new token when expired
⚠️ Scope limited: Primarily imchat:bot scope
Comparison Table
| Feature | S2S OAuth | User OAuth | Device Flow | Chatbot |
|---|---|---|---|---|
| Grant Type | account_credentials | authorization_code | device_code | client_credentials |
| User Interaction | No | Yes (browser) | Yes (separate device) | No |
| Access Token Lifetime | 1 hour | 1 hour | 1 hour | 1 hour |
| Refresh Token | ❌ None | ✅ ~90 days (commonly) | ✅ ~90 days (commonly) | ❌ None |
| Redirect URI | ❌ Not needed | ✅ Required | ❌ Not needed | ❌ Not needed |
| PKCE Support | ❌ N/A | ✅ Optional | ❌ N/A | ❌ N/A |
| State Parameter | ❌ N/A | ✅ Recommended | ❌ N/A | ❌ N/A |
| Account Access | Account-wide | Per-user | Per-user | Account-wide |
| Token Storage | Redis (ephemeral) | Database (persistent) | Database (persistent) | Redis (ephemeral) |
| Use Case | Backend automation | SaaS apps | TV/kiosk apps | Chat bots |
OAuth 2.0 Standards
Zoom OAuth follows these RFCs:
RFC 6749: OAuth 2.0 Authorization Framework
RFC 7636: PKCE (Proof Key for Code Exchange)
RFC 8628: Device Authorization Grant
Next Steps
Understand token lifecycle → token-lifecycle.md
Learn about PKCE → pkce.md
Implement your flow:
Device → ../examples/device-flow.md
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Zoom
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/zoom/skills/oauth · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-oauth.zip