Zoom OAuth

SkillSecurity

Zoom OAuth authentication and authorization. Covers four authorization flows: Account (S2S), User (authorization code), Device (device flow), and Client (chatbot). Use when implementing authentication for Zoom APIs, managing access tokens, or understanding OAuth flows.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Zoom OAuth skill

What this skill tells your AI

The instructions your AI receives, as published by zoom/skills in skills/oauth/SKILL.md and read by ahel’s review.

Authentication and authorization for Zoom APIs.

📖 Complete Documentation

For comprehensive guides, production patterns, and troubleshooting, see Integrated Index section below.

Quick navigation:

Prerequisites

  • Zoom app created in Marketplace
  • Client ID and Client Secret
  • For S2S OAuth: Account ID

Need to create/select the app first? Use Marketplace app management for API-driven app creation, manifest validation, app type selection, app-owned Marketplace scopes, and credential response shapes before implementing token flows. Use the template selector for S2S, user-managed, admin-managed, and app-owned credential scenarios. Automation services should also inspect the machine-readable template index before selecting an app type.

Four Authorization Use Cases

Use CaseApp TypeGrant TypeIndustry Name
Account AuthorizationServer-to-Serveraccount_credentialsClient Credentials Grant, M2M, Two-legged OAuth
User AuthorizationGeneralauthorization_codeAuthorization Code Grant, Three-legged OAuth
Device AuthorizationGeneralurn:ietf:params:oauth:grant-type:device_codeDevice Authorization Grant (RFC 8628)
Client AuthorizationGeneralclient_credentialsApp-owned access token for chatbots and selected Marketplace app scopes

Industry Terminology

TermMeaning
Two-legged OAuthNo user involved (client ↔ server)
Three-legged OAuthUser involved (user ↔ client ↔ server)
M2MMachine-to-Machine (backend services)
Public clientCan't keep secrets (mobile, SPA) → use PKCE
Confidential clientCan keep secrets (backend servers)
PKCEProof Key for Code Exchange (RFC 7636), pronounced "pixy"

Which Flow Should I Use?

                              ┌─────────────────────┐
                              │  What are you       │
                              │  building?          │
                              └──────────┬──────────┘
                                         │
                    ┌────────────────────┼────────────────────┐
                    │                    │                    │
                    ▼                    ▼                    ▼
          ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
          │  Backend        │  │  App for other  │  │  App-owned API  │
          │  automation     │  │  users/accounts │  │  (Team Chat)    │
          │  (your account) │  │                 │  │                 │
          └────────┬────────┘  └────────┬────────┘  └────────┬────────┘
                   │                    │                    │
                   ▼                    │                    ▼
          ┌─────────────────┐           │           ┌─────────────────┐
          │    ACCOUNT      │           │           │     CLIENT      │
          │   (S2S OAuth)   │           │           │  (app-owned)    │
          └─────────────────┘           │           └─────────────────┘
                                        │
                                        ▼
                              ┌─────────────────────┐
                              │  Does device have   │
                              │  a browser?         │
                              └──────────┬──────────┘
                                         │
                         ┌───────────────┴───────────────┐
                         │ NO                         YES│
                         ▼                               ▼
          ┌─────────────────────────┐         ┌─────────────────┐
          │        DEVICE           │         │      USER       │
          │     (Device Flow)       │         │  (Auth Code)    │
          │                         │         │                 │
          │ Examples:               │         │ + PKCE if       │
          │ • Smart TV              │         │   public client │
          │ • Meeting SDK device    │         │                 │
          └─────────────────────────┘         └─────────────────┘

Account Authorization (Server-to-Server OAuth)

For backend automation without user interaction.

Request Access Token

POST https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ACCOUNT_ID}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Response

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scope": "user:read:user:admin",
  "api_url": "https://api.zoom.us"
}

Refresh

Access tokens expire after 1 hour. No separate refresh flow - just request a new token.


User Authorization (Authorization Code Flow)

For apps that act on behalf of users.

Step 1: Redirect User to Authorize

https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}

Use https://zoom.us/oauth/authorize for consent, but https://zoom.us/oauth/token for token exchange.

Optional Parameters:

ParameterDescription
stateCSRF protection, maintains state through flow
code_challengeFor PKCE (see below)
code_challenge_methodS256 or plain (default: plain)

Step 2: User Authorizes

  • User signs in and grants permission
  • Redirects to redirect_uri with authorization code:
    https://example.com/?code={AUTHORIZATION_CODE}
    

Step 3: Exchange Code for Token

POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

With PKCE: Add code_verifier parameter.

Response

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "refresh_token": "eyJ...",
  "expires_in": 3600,
  "scope": "user:read:user",
  "api_url": "https://api.zoom.us"
}

Refresh Token

POST https://zoom.us/oauth/token?grant_type=refresh_token&refresh_token={REFRESH_TOKEN}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
  • Access tokens expire after 1 hour
  • Refresh token lifetime can vary; ~90 days is common for some user-based flows. Treat it as configuration/behavior that can change and rely on runtime errors + re-auth fallback.
  • Always use the latest refresh token for the next request
  • If refresh token expires, redirect user to authorization URL to restart flow

User-Level vs Account-Level Apps

TypeWho Can AuthorizeScope Access
User-levelAny individual userScoped to themselves
Account-levelUser with admin permissionsAccount-wide access (admin scopes)

Device Authorization (Device Flow)

For devices without browsers (e.g., Meeting SDK apps).

Prerequisites

Enable "Use App on Device" in: Features > Embed > Enable Meeting SDK

Step 1: Request Device Code

POST https://zoom.us/oauth/devicecode?client_id={CLIENT_ID}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Response

{
  "device_code": "DEVICE_CODE",
  "user_code": "abcd1234",
  "verification_uri": "https://zoom.us/oauth_device",
  "verification_uri_complete": "https://zoom.us/oauth/device/complete/{CODE}",
  "expires_in": 900,
  "interval": 5
}

Step 2: User Authorization

Direct user to:

  • verification_uri and display user_code for manual entry, OR
  • verification_uri_complete (user code prefilled)

User signs in and allows the app.

Step 3: Poll for Token

Poll at the interval (5 seconds) until user authorizes:

POST https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code={DEVICE_CODE}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Response

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "refresh_token": "eyJ...",
  "expires_in": 3599,
  "scope": "user:read:user user:read:token",
  "api_url": "https://api.zoom.us"
}

Polling Responses

ResponseMeaningAction
Token returnedUser authorizedStore tokens, done
error: authorization_pendingUser hasn't authorized yetKeep polling at interval
error: slow_downPolling too fastIncrease interval by 5 seconds
error: expired_tokenDevice code expired (15 min)Restart flow from Step 1
error: access_deniedUser denied authorizationHandle denial, don't retry

Polling Implementation

async function pollForToken(deviceCode, interval) {
  while (true) {
    await sleep(interval * 1000);

    try {
      const response = await axios.post(
        `https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
        null,
        { headers: { 'Authorization': `Basic ${credentials}` } }
      );
      return response.data; // Success - got tokens
    } catch (error) {
      const err = error.response?.data?.error;
      if (err === 'authorization_pending') continue;
      if (err === 'slow_down') { interval += 5; continue; }
      throw error; // expired_token or access_denied
    }
  }
}

Refresh

Same as User Authorization. If refresh token expires, restart device flow from Step 1.


Client Authorization (App-Owned Access Tokens)

For app-owned operations that do not act on behalf of a specific user. Historically this was most commonly used for Team Chat bot operations, but selected Marketplace app-management scopes also require this grant.

Use client_credentials for:

  • Team Chat bot operations such as imchat:bot
  • Marketplace app-owned scopes such as marketplace:write:event_subscription
  • Marketplace event subscription management scopes such as marketplace:read:list_event_subscriptions, marketplace:update:event_subscription, and marketplace:delete:event_subscription
  • Marketplace WebSocket connection scope marketplace:write:websocket_connection

Do not confuse this with Server-to-Server OAuth. S2S uses grant_type=account_credentials plus account_id; app-owned client authorization uses grant_type=client_credentials without account_id.

Request Token

POST https://zoom.us/oauth/token?grant_type=client_credentials

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Response

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scope": "imchat:bot marketplace:write:event_subscription",
  "api_url": "https://api.zoom.us"
}

Refresh

Tokens expire after 1 hour. No refresh flow - just request a new token.


Using Access Tokens

Call API

GET https://api.zoom.us/v2/users/me

Headers:
Authorization: Bearer {ACCESS_TOKEN}

Me Context

Replace userID with me to target the token's associated user:

EndpointMethods
/v2/users/meGET, PATCH
/v2/users/me/tokenGET
/v2/users/me/meetingsGET, POST

Revoke Access Token

Works for all authorization types.

POST https://zoom.us/oauth/revoke?token={ACCESS_TOKEN}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Response

{
  "status": "success"
}

PKCE (Proof Key for Code Exchange)

For public clients that can't securely store secrets (mobile apps, SPAs, desktop apps).

When to Use PKCE

Client TypeUse PKCE?Why
Mobile appYesCan't securely store client secret
Single Page App (SPA)YesJavaScript is visible to users
Desktop appYesBinary can be decompiled
Meeting SDK (client-side)YesRuns on user's device
Backend serverOptionalCan keep secrets, but PKCE adds security

How PKCE Works

┌──────────┐                              ┌──────────┐                    ┌──────────┐
│  Client  │                              │   Zoom   │                    │   Zoom   │
│   App    │                              │  Auth    │                    │  Token   │
└────┬─────┘                              └────┬─────┘                    └────┬─────┘
     │                                         │                              │
     │ 1. Generate code_verifier (random)      │                              │
     │ 2. Create code_challenge = SHA256(verifier)                            │
     │                                         │                              │
     │ ─────── /authorize + code_challenge ──► │                              │
     │                                         │                              │
     │ ◄────── authorization_code ──────────── │                              │
     │                                         │                              │
     │ ─────────────── /token + code_verifier ─┼────────────────────────────► │
     │                                         │                              │
     │                                         │     Verify: SHA256(verifier) │
     │                                         │            == challenge      │
     │                                         │                              │
     │ ◄───────────────────────────────────────┼─────── access_token ──────── │
     │                                         │                              │

Implementation (Node.js)

const crypto = require('crypto');

function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
  return { verifier, challenge };
}

const pkce = generatePKCE();

const authUrl = `https://zoom.us/oauth/authorize?` +
  `response_type=code&` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${REDIRECT_URI}&` +
  `code_challenge=${pkce.challenge}&` +
  `code_challenge_method=S256`;

// Store pkce.verifier in session for callback

Token Exchange with PKCE

POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI}&code_verifier={VERIFIER}

Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}

Deauthorization

When a user removes your app, Zoom sends a webhook to your Deauthorization Notification Endpoint URL.

Webhook Event

{
  "event": "app_deauthorized",
  "event_ts": 1740439732278,
  "payload": {
    "account_id": "ACCOUNT_ID",
    "user_id": "USER_ID",
    "signature": "SIGNATURE",
    "deauthorization_time": "2019-06-17T13:52:28.632Z",
    "client_id": "CLIENT_ID"
  }
}

Requirements

  • Delete all associated user data after receiving this event
  • Verify webhook signature (use secret token, verification token deprecated Oct 2023)
  • Only public apps receive deauthorization webhooks (not private/dev apps)

Pre-Approval Flow

Some Zoom accounts require Marketplace admin pre-approval before users can authorize apps.

  • Users can request pre-approval from their admin
  • Account-level apps (admin scopes) require appropriate role permissions

Active Apps Notifier (AAN)

In-meeting feature showing apps with real-time access to content.

  • Displays icon + tooltip with app info, content type being accessed, approving account
  • Supported: Zoom client 5.6.7+, Meeting SDK 5.9.0+

OAuth Scopes

Scope Types

TypeDescriptionFor
Classic scopesLegacy scopes (user, admin, master levels)Existing apps
Granular scopesNew fine-grained scopes with optional supportNew apps

Classic Scopes

For previously-created apps. Three levels:

  • User-level: Access to individual user's data
  • Admin-level: Account-wide access, requires admin role
  • Master-level: For master-sub account setups, requires account owner

Full list: https://developers.zoom.us/docs/integrations/oauth-scopes/

Granular Scopes

For new apps. Format: <service>:<action>:<data_claim>:<access>

ComponentValues
servicemeeting, webinar, user, recording, etc.
actionread, write, update, delete
data_claimData category (e.g., participants, settings)
accessempty (user), admin, master

Example: meeting:read:list_meetings:admin

Full list: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/

Optional Scopes

Granular scopes can be marked as optional - users choose whether to grant them.

Basic authorization (uses build flow defaults):

https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}

Advanced authorization (custom scopes per request):

https://zoom.us/oauth/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={required_scopes}&optional_scope={optional_scopes}

Include previously granted scopes:

https://zoom.us/oauth/authorize?...&include_granted_scopes&scope={additional_scopes}

Migrating Classic to Granular

  1. Manage > select app > edit
  2. Scope page > Development tab > click Migrate
  3. Review auto-assigned granular scopes, remove unnecessary, mark optional
  4. Test
  5. Production tab > click Migrate

Notes:

  • No review needed if only migrating or reducing scopes
  • Existing user tokens continue with classic scope values until re-authorization
  • New users get granular scopes after migration

Common Error Codes

CodeMessageSolution
4700Token cannot be emptyCheck Authorization header has valid token
4702/4704Invalid clientVerify Client ID and Client Secret
4705Grant type not supportedUse: account_credentials, authorization_code, urn:ietf:params:oauth:grant-type:device_code, or client_credentials
4706Client ID or secret missingAdd credentials to header or request params
4709Redirect URI mismatchEnsure redirect_uri matches app configuration exactly (including trailing slash)
4711Refresh token invalidToken scopes don't match client scopes
4717App has been disabledContact Zoom support
4733Code is expiredAuthorization codes expire in 5 minutes - restart flow
4734Invalid authorization codeRegenerate authorization code
4735Owner of token does not existUser was removed from account - re-authorize
4741Token has been revokedUse the most recent token from latest authorization

See references/oauth-errors.md for complete error list.


Quick Reference

FlowGrant TypeToken ExpiryRefresh
Account (S2S)account_credentials1 hourRequest new token
Userauthorization_code1 hourUse refresh_token (90 day expiry)
Deviceurn:ietf:params:oauth:grant-type:device_code1 hourUse refresh_token (90 day expiry)
Client (app-owned)client_credentials1 hourRequest new token

Demo Guidance

If you build an OAuth demo app, document its runtime base URL in that demo project's own README or .env.example, not in this shared skill.

Resources


Integrated Index

This section was migrated from SKILL.md.

Quick Start Path

If you're new to Zoom OAuth, follow this order:

  1. Run preflight checks firstRUNBOOK.md

  2. Choose your OAuth flowconcepts/oauth-flows.md

    • 4 token flows: S2S account credentials, General App authorization code, Device, and General App client credentials
    • Decision matrix: Which flow fits your use case?
  3. Understand token lifecycleconcepts/token-lifecycle.md

    • CRITICAL: How tokens expire, refresh, and revoke
    • Common pitfalls: refresh token rotation
  4. Implement your flow → Jump to examples:

  5. Fix redirect URI issuestroubleshooting/redirect-uri-issues.md

    • Most common OAuth error: Redirect URI mismatch
  6. Implement token refreshexamples/token-refresh.md

    • Automatic middleware pattern
    • Handle refresh token rotation
  7. Troubleshoot errorstroubleshooting/common-errors.md

    • Error code tables (4700-4741 range)
    • Quick diagnostic workflow

Documentation Structure

oauth/
├── SKILL.md                           # Main skill overview
├── SKILL.md                           # This file - navigation guide
│
├── concepts/                          # Core OAuth concepts
│   ├── oauth-flows.md                # 4 flows: S2S, authorization code, device, client credentials
│   ├── token-lifecycle.md            # Expiration, refresh, revocation
│   ├── pkce.md                       # PKCE security for public clients
│   ├── scopes-architecture.md        # Classic vs Granular scopes
│   └── state-parameter.md            # CSRF protection with state
│
├── examples/                          # Complete working code
│   ├── s2s-oauth-basic.md            # S2S OAuth minimal example
│   ├── s2s-oauth-redis.md            # S2S OAuth with Redis caching (production)
│   ├── user-oauth-basic.md           # User OAuth minimal example
│   ├── user-oauth-mysql.md           # User OAuth with MySQL + encryption (production)
│   ├── device-flow.md                # Device authorization flow
│   ├── pkce-implementation.md        # PKCE for SPAs/mobile apps
│   └── token-refresh.md              # Auto-refresh middleware pattern
│
├── troubleshooting/                   # Problem solving guides
│   ├── common-errors.md              # Error codes 4700-4741
│   ├── redirect-uri-issues.md        # Most common OAuth error
│   ├── token-issues.md               # Expired, revoked, invalid tokens
│   └── scope-issues.md               # Scope mismatch errors
│
└── references/                        # Reference documentation
    ├── oauth-errors.md                # Complete error code reference
    ├── classic-scopes.md              # Classic scope reference
    └── granular-scopes.md             # Granular scope reference

By Use Case

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
77
Forks
16
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages (in examples/s2s-oauth-redis.md)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
zoom-oauth
Source
github.com/zoom/skills