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://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/token-lifecycle.md
Token Lifecycle
Understanding how Zoom OAuth tokens are created, expire, refresh, and revoke is critical for building reliable integrations.
Token Types
Access Token
Purpose: Authenticate API requests
Lifetime: 1 hour (all OAuth flows)
Usage:
Authorization: Bearer {access_token}headerFormat: Opaque string (not JWT)
Refresh Token
Purpose: Obtain new access tokens without user re-authorization
Lifetime: Varies by flow/account/app configuration; ~90 days is common for some user-based flows (treat as changeable behavior)
Availability: S2S OAuth and Chatbot do NOT have refresh tokens
Rotation: Each refresh returns a NEW refresh token (old one becomes invalid)
Authorization Code
Purpose: Temporary code exchanged for access token
Lifetime: 5 minutes
Usage: User OAuth and Device Flow only
One-time use: Code becomes invalid after exchange
Expiration Summary
| Flow | Access Token | Refresh Token | Strategy |
|---|---|---|---|
| S2S OAuth | 1 hour | None | Request new token before expiration |
| User OAuth | 1 hour | ~90 days (commonly) | Use refresh token to get new access token |
| Device Flow | 1 hour | ~90 days (commonly) | Use refresh token to get new access token |
| Chatbot | 1 hour | None | Request new token before expiration |
S2S OAuth & Chatbot Token Lifecycle
Timeline
┌────────────────────────────────────────────────────┐
│ │
│ Token Request │
│ │ │
│ v │
│ [ Access Token Valid ] │
│ │
│ ├───────────────────── 1 hour ──────────────────┤ │
│ │
│ Token │
│ Expires │
│ │ │
│ v │
│ Request New Token ────────────────> [ New Access Token Valid ]
│ │
└────────────────────────────────────────────────────┘Strategy: Cache with TTL
const redis = require('redis');
const client = redis.createClient();
const getToken = async () => {
// Check cache first
let token = await client.get('zoom_access_token');
if (!token) {
// Request new token
const response = await axios.post('https://zoom.us/oauth/token', ...);
const { access_token, expires_in } = response.data;
// Cache with TTL (10 second buffer before actual expiration)
await client.setex('zoom_access_token', expires_in - 10, access_token);
token = access_token;
}
return token;
};Key Points:
✅ Cache token in Redis with TTL matching expiration
✅ Use 10-second buffer to prevent race conditions
✅ Single token shared across all requests
❌ Do NOT request new token on every API call
❌ Do NOT try to "refresh" (no refresh token exists)
User OAuth & Device Flow Token Lifecycle
Timeline
┌────────────────────────────────────────────────────────────────────┐
│ │
│ User Authorizes │
│ │ │
│ v │
│ [ Access Token Valid ] │
│ [ Refresh Token Valid ]────────────────────────────────────────┐ │
│ │ │
│ ├───────────── 1 hour ────────────┤ │ │
│ │ │
│ Access Token Expires │ │
│ │ │ │
│ v │ │
│ Refresh Request ──────────> [ New Access Token Valid ] │ │
│ [ New Refresh Token Valid ]────┐ │ │
│ │ │ │
│ ├───────────── 1 hour ────────────┤ │ │ │
│ │ │ │
│ Access Token Expires │ │ │
│ │ │ │ │
│ v │ │ │
│ Refresh Request ──────────> [ New Access Token Valid ] │ │ │
│ [ New Refresh Token Valid ] │ │ │
│ │ │ │
│ ... Continue refreshing up to ~90 days (commonly) ... │ │ │
│ │ │ │
│ ├──────────────────────── ~90 days (commonly) ───────────┤ │ │
│ │ │
│ Refresh Token Expires │ │
│ │ │ │
│ v │ │
│ User Must Re-Authorize (restart OAuth flow) │ │
│ │
└────────────────────────────────────────────────────────────────────┘Strategy: Auto-Refresh Middleware
const tokenMiddleware = async (req, res, next) => {
const userId = req.session.userId;
// Get user's tokens from database
let { access_token, refresh_token, token_expiry } = await getUserTokens(userId);
// Check if access token is expired or will expire soon (5 minute buffer)
const now = Date.now();
const expiresIn = token_expiry - now;
if (expiresIn < 300000) { // Less than 5 minutes remaining
// Refresh the token
const response = await axios.post(
'https://zoom.us/oauth/token',
qs.stringify({
grant_type: 'refresh_token',
refresh_token: refresh_token
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${CLIENT_ID}:${CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const { access_token: new_access_token, refresh_token: new_refresh_token, expires_in } = response.data;
// CRITICAL: Update BOTH tokens in database
await updateUserTokens(userId, {
access_token: new_access_token,
refresh_token: new_refresh_token, // NEW refresh token
token_expiry: now + (expires_in * 1000)
});
access_token = new_access_token;
}
// Attach token to request
req.zoomToken = access_token;
next();
};Key Points:
✅ Refresh BEFORE token expires (5-minute buffer recommended)
✅ ALWAYS save the NEW refresh token (old one becomes invalid)
✅ Store tokens per user in database
✅ Encrypt tokens at rest (AES-256 minimum)
❌ Do NOT reuse old refresh token after refresh
❌ Do NOT wait for API 401 errors to trigger refresh
Refresh Token Rotation
CRITICAL: Zoom rotates refresh tokens on every refresh.
What Happens During Refresh
Before Refresh:
access_token: "abc123" (expired)
refresh_token: "xyz789" (valid)
Request:
POST /oauth/token
grant_type=refresh_token
refresh_token=xyz789
Response:
{
"access_token": "def456", // NEW access token
"refresh_token": "uvw012", // NEW refresh token
"expires_in": 3600
}
After Refresh:
access_token: "def456" (valid for 1 hour)
refresh_token: "uvw012" (lifetime varies; ~90 days is common)
OLD refresh_token "xyz789" is NOW INVALIDCommon Mistake
// ❌ WRONG: Not saving new refresh token
const response = await refreshToken(old_refresh_token);
const { access_token } = response.data; // Only saving access token
await updateUserTokens(userId, { access_token }); // Refresh token not updated!
// Next refresh will fail with error 4735 "Invalid refresh token"// ✅ CORRECT: Saving both tokens
const response = await refreshToken(old_refresh_token);
const { access_token, refresh_token } = response.data;
await updateUserTokens(userId, {
access_token,
refresh_token // MUST save new refresh token
});Authorization Code Expiration
Lifetime: 5 minutes
Timeline
User Clicks "Allow"
│
v
Authorization Code Issued (expires in 5 minutes)
│
│ ← Exchange code for token within 5 minutes
v
[ Access Token + Refresh Token ]
If code not exchanged within 5 minutes:
→ Error 4733 "Invalid authorization code"
→ User must re-authorizeImplementation
app.get('/callback', async (req, res) => {
const { code } = req.query;
try {
// Exchange code for token IMMEDIATELY
const response = await axios.post('https://zoom.us/oauth/token', {
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.REDIRECT_URI
}, ...);
// Store tokens
await saveTokens(response.data);
} catch (error) {
if (error.response?.data?.error === 'invalid_grant') {
// Code expired (4733) or already used
res.send('Authorization code expired. Please re-authorize.');
}
}
});Key Points:
✅ Exchange authorization code immediately upon receiving it
✅ Authorization codes are one-time use
❌ Do NOT cache or store authorization codes
❌ Do NOT delay token exchange
Token Revocation
When Tokens Are Revoked
User re-authorizes your app:
All previous tokens for that user become invalid
New tokens are issued
User removes your app:
All tokens for that user become invalid
User must re-authorize to grant access again
Explicit revocation:
Your app calls
https://zoom.us/oauth/revokeendpointTokens become invalid immediately
Refresh token expires (lifetime varies):
Can no longer refresh
User must re-authorize
Revoke Token API
const revokeToken = async (access_token) => {
await axios.post(
'https://zoom.us/oauth/revoke',
qs.stringify({
token: access_token
}),
{
headers: {
'Authorization': `Basic ${Buffer.from(
`${CLIENT_ID}:${CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
// Token is now revoked
// Delete from database
await deleteUserTokens(userId);
};What Gets Revoked:
Access token becomes invalid immediately
Refresh token becomes invalid immediately
All API requests with revoked token return 401
Error Codes
| Code | Error | Meaning | Action |
|---|---|---|---|
| 4733 | Invalid authorization code | Code expired (5 min) or already used | User must re-authorize |
| 4735 | Invalid refresh token | Refresh token expired or rotated | User must re-authorize |
| 4741 | Token has been revoked | Token was explicitly revoked | User must re-authorize |
| 401 | Unauthorized | Access token expired or invalid | Refresh token (if available) or re-authorize |
Best Practices
1. Cache S2S Tokens
// ✅ Cache in Redis with TTL
await redis.setex('zoom_token', expires_in - 10, access_token);// ❌ Request new token on every API call
const token = await getToken(); // Every time? No!
await makeAPIRequest(token);2. Refresh BEFORE Expiration
// ✅ Refresh with buffer (5 minutes before expiry)
if (expiresIn < 300000) {
await refreshToken();
}// ❌ Wait for 401 error
try {
await makeAPIRequest(token);
} catch (err) {
if (err.status === 401) {
await refreshToken(); // Too late!
}
}3. Always Save New Refresh Token
// ✅ Update both tokens
const { access_token, refresh_token } = await refresh();
await saveTokens({ access_token, refresh_token });// ❌ Only save access token
const { access_token } = await refresh();
await saveTokens({ access_token }); // Refresh token not saved!4. Encrypt Tokens at Rest
// ✅ Encrypt before storing
const encrypted = encrypt(access_token, CIPHER_KEY);
await db.query('UPDATE users SET token = ? WHERE id = ?', [encrypted, userId]);// ❌ Store in plain text
await db.query('UPDATE users SET token = ? WHERE id = ?', [access_token, userId]);5. Handle Revocation Gracefully
// ✅ Detect revoked tokens and prompt re-auth
if (error.code === 4741) {
await deleteUserTokens(userId);
res.redirect('/auth'); // Re-authorize
}Debugging Token Issues
Symptom: "Token expired" immediately after getting it
Cause: Server clock is incorrect
Solution:
# Sync server time
sudo ntpdate -s time.nist.govSymptom: Refresh fails with "Invalid refresh token" (4735)
Cause: Using old refresh token after it was rotated
Solution:
Check database: Are you saving the NEW refresh token?
Check code: Are you updating BOTH access_token AND refresh_token?
Symptom: Authorization code fails with "Invalid grant" (4733)
Cause: Code expired (5 minutes passed) or already used
Solution:
Exchange code immediately in callback
Codes are one-time use (don't cache)
Symptom: All tokens revoked unexpectedly
Cause: User re-authorized your app or removed it
Solution:
Detect 401/4741 errors
Prompt user to re-authorize
Next Steps
Implement auto-refresh → ../examples/token-refresh.md
Fix token errors → ../troubleshooting/token-issues.md
Understand OAuth flows → oauth-flows.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