Generated banner for Zoom Oauth
Generated logo for Zoom Oauth

Zoom Oauth

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • No issues found

From the author

Use when implementing OAuth.
— the description by Zoom

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

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/openai/zoom-oauth/download
  2. Unzip 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

  1. 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.

  2. Confirm the target API, SDK, or app surface, because scopes and token audiences differ by surface.

  3. 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.

  4. Store refresh tokens as single-use values: persist the replacement refresh token returned by each refresh response before reusing the old one.

  5. Validate requests against redirect URI, account ID, scopes, app publication state, and token expiration before changing application code.

  6. For local development, keep access tokens out of logs and treat refresh tokens as single-use when rotating credentials.

References

Files

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} header

  • Format: Opaque string (not JWT)

Refresh Token
  • Purpose: Obtain new access tokens without user re-authorization

  • Lifetime: Varies by flow/account/app configuration; ~90 days is common for some user-based flows (treat as changeable behavior)

  • Availability: S2S OAuth and Chatbot do NOT have refresh tokens

  • Rotation: Each refresh returns a NEW refresh token (old one becomes invalid)

Authorization Code
  • Purpose: Temporary code exchanged for access token

  • Lifetime: 5 minutes

  • Usage: User OAuth and Device Flow only

  • One-time use: Code becomes invalid after exchange


Expiration Summary

FlowAccess TokenRefresh TokenStrategy
S2S OAuth1 hourNoneRequest new token before expiration
User OAuth1 hour~90 days (commonly)Use refresh token to get new access token
Device Flow1 hour~90 days (commonly)Use refresh token to get new access token
Chatbot1 hourNoneRequest 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 INVALID
Common 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-authorize
Implementation
app.get('/callback', async (req, res) => {
  const { code } = req.query;

  try {
    // Exchange code for token IMMEDIATELY
    const response = await axios.post('https://zoom.us/oauth/token', {
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: process.env.REDIRECT_URI
    }, ...);

    // Store tokens
    await saveTokens(response.data);

  } catch (error) {
    if (error.response?.data?.error === 'invalid_grant') {
      // Code expired (4733) or already used
      res.send('Authorization code expired. Please re-authorize.');
    }
  }
});

Key Points:

  • ✅ Exchange authorization code immediately upon receiving it

  • ✅ Authorization codes are one-time use

  • ❌ Do NOT cache or store authorization codes

  • ❌ Do NOT delay token exchange


Token Revocation

When Tokens Are Revoked
  1. User re-authorizes your app:

    • All previous tokens for that user become invalid

    • New tokens are issued

  2. User removes your app:

    • All tokens for that user become invalid

    • User must re-authorize to grant access again

  3. Explicit revocation:

    • Your app calls https://zoom.us/oauth/revoke endpoint

    • Tokens become invalid immediately

  4. Refresh token expires (lifetime varies):

    • Can no longer refresh

    • User must re-authorize

Revoke Token API
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

CodeErrorMeaningAction
4733Invalid authorization codeCode expired (5 min) or already usedUser must re-authorize
4735Invalid refresh tokenRefresh token expired or rotatedUser must re-authorize
4741Token has been revokedToken was explicitly revokedUser must re-authorize
401UnauthorizedAccess token expired or invalidRefresh 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.gov
Symptom: Refresh fails with "Invalid refresh token" (4735)

Cause: Using old refresh token after it was rotated

Solution:

  • Check database: Are you saving the NEW refresh token?

  • Check code: Are you updating BOTH access_token AND refresh_token?

Symptom: Authorization code fails with "Invalid grant" (4733)

Cause: Code expired (5 minutes passed) or already used

Solution:

  • Exchange code immediately in callback

  • Codes are one-time use (don't cache)

Symptom: All tokens revoked unexpectedly

Cause: User re-authorized your app or removed it

Solution:

  • Detect 401/4741 errors

  • Prompt user to re-authorize


Next Steps

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