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/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/authorize

  • Token URL: https://zoom.us/oauth/token

Quick Decision Matrix

Your ScenarioFlowGrant Type
Backend automation on your own accountS2S OAuthaccount_credentials
SaaS app for other Zoom usersUser OAuthauthorization_code
Device without browser (TV, kiosk, IoT)Device Flowurn:ietf:params:oauth:grant-type:device_code
Team Chat bot onlyChatbotclient_credentials

Two-Legged vs Three-Legged

TypeUser Involved?Zoom Flows
Two-leggedNo (app acts on its own)S2S OAuth, Chatbot
Three-leggedYes (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:bot scope

  • Simpler than S2S OAuth

Grant type: client_credentials

Token lifetime:

  • Access token: 1 hour

  • Refresh token: None (request new token when expired)

Credentials required:

  • Client ID

  • Client Secret

Flow Diagram
┌──────────────┐                                    ┌──────────────┐
│ Chatbot App  │                                    │  Zoom OAuth  │
│  (Backend)   │                                    │    Server    │
└──────┬───────┘                                    └──────┬───────┘
       │                                                   │
       │  POST /oauth/token                                │
       │  grant_type=client_credentials                    │
       │  Authorization: Basic {CLIENT_ID:CLIENT_SECRET}   │
       │──────────────────────────────────────────────────>│
       │                                                   │
       │  { access_token, expires_in, scope }              │
       │<──────────────────────────────────────────────────│
       │                                                   │
       │  Chatbot API Requests with Bearer token           │
       │  (valid for 1 hour)                               │
       │                                                   │
Implementation
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

FeatureS2S OAuthUser OAuthDevice FlowChatbot
Grant Typeaccount_credentialsauthorization_codedevice_codeclient_credentials
User InteractionNoYes (browser)Yes (separate device)No
Access Token Lifetime1 hour1 hour1 hour1 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 AccessAccount-widePer-userPer-userAccount-wide
Token StorageRedis (ephemeral)Database (persistent)Database (persistent)Redis (ephemeral)
Use CaseBackend automationSaaS appsTV/kiosk appsChat bots

OAuth 2.0 Standards

Zoom OAuth follows these RFCs:


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