zoom-oauth

Reference skill for Zoom authentication. Use after routing to an auth workflow when choosing app credentials, grant types, scopes, token refresh behavior, or…

INSTALLATION
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill zoom-oauth
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Prerequisites

  • Client ID and Client Secret
  • For S2S OAuth: Account ID

Four Authorization Use Cases

Use Case

App Type

Grant Type

Industry Name

Account Authorization

Server-to-Server

account_credentials

Client Credentials Grant, M2M, Two-legged OAuth

User Authorization

General

authorization_code

Authorization Code Grant, Three-legged OAuth

Device Authorization

General

urn:ietf:params:oauth:grant-type:device_code

Device Authorization Grant (RFC 8628)

Client Authorization

General

client_credentials

Client Credentials Grant (chatbot-scoped)

Industry Terminology

Term

Meaning

Two-legged OAuth

No user involved (client ↔ server)

Three-legged OAuth

User involved (user ↔ client ↔ server)

M2M

Machine-to-Machine (backend services)

Public client

Can't keep secrets (mobile, SPA) → use PKCE

Confidential client

Can keep secrets (backend servers)

PKCE

Proof Key for Code Exchange (RFC 7636), pronounced "pixy"

Which Flow Should I Use?

┌─────────────────────┐

                              │  What are you       │

                              │  building?          │

                              └──────────┬──────────┘

                                         │

                    ┌────────────────────┼────────────────────┐

                    │                    │                    │

                    ▼                    ▼                    ▼

          ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐

          │  Backend        │  │  App for other  │  │  Chatbot only   │

          │  automation     │  │  users/accounts │  │  (Team Chat)    │

          │  (your account) │  │                 │  │                 │

          └────────┬────────┘  └────────┬────────┘  └────────┬────────┘

                   │                    │                    │

                   ▼                    │                    ▼

          ┌─────────────────┐           │           ┌─────────────────┐

          │    ACCOUNT      │           │           │     CLIENT      │

          │   (S2S OAuth)   │           │           │   (Chatbot)     │

          └─────────────────┘           │           └─────────────────┘

                                        │

                                        ▼

                              ┌─────────────────────┐

                              │  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:

Parameter

Description

state

CSRF protection, maintains state through flow

code_challenge

For PKCE (see below)

code_challenge_method

S256 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

Type

Who Can Authorize

Scope Access

User-level

Any individual user

Scoped to themselves

Account-level

User with admin permissions

Account-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

Response

Meaning

Action

Token returned

User authorized

Store tokens, done

error: authorization_pending

User hasn't authorized yet

Keep polling at interval

error: slow_down

Polling too fast

Increase interval by 5 seconds

error: expired_token

Device code expired (15 min)

Restart flow from Step 1

error: access_denied

User denied authorization

Handle 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 (Chatbot)

For chatbot message operations only.

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",

  "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:

Endpoint

Methods

/v2/users/me

GET, PATCH

/v2/users/me/token

GET

/v2/users/me/meetings

GET, 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 Type

Use PKCE?

Why

Mobile app

Yes

Can't securely store client secret

Single Page App (SPA)

Yes

JavaScript is visible to users

Desktop app

Yes

Binary can be decompiled

Meeting SDK (client-side)

Yes

Runs on user's device

Backend server

Optional

Can 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

Type

Description

For

Classic scopes

Legacy scopes (user, admin, master levels)

Existing apps

Granular scopes

New fine-grained scopes with optional support

New 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>

Component

Values

service

meeting, webinar, user, recording, etc.

action

read, write, update, delete

data_claim

Data category (e.g., participants, settings)

access

empty (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&#x26;client_id={CLIENT_ID}&#x26;redirect_uri={REDIRECT_URI}

Advanced authorization (custom scopes per request):

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

Include previously granted scopes:

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

Migrating Classic to Granular

  • Manage > select app > edit
  • Scope page > Development tab > click Migrate
  • Review auto-assigned granular scopes, remove unnecessary, mark optional
  • Test
  • 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

Code

Message

Solution

4700

Token cannot be empty

Check Authorization header has valid token

4702/4704

Invalid client

Verify Client ID and Client Secret

4705

Grant type not supported

Use: account_credentials, authorization_code, urn:ietf:params:oauth:grant-type:device_code, or client_credentials

4706

Client ID or secret missing

Add credentials to header or request params

4709

Redirect URI mismatch

Ensure redirect_uri matches app configuration exactly (including trailing slash)

4711

Refresh token invalid

Token scopes don't match client scopes

4717

App has been disabled

Contact Zoom support

4733

Code is expired

Authorization codes expire in 5 minutes - restart flow

4734

Invalid authorization code

Regenerate authorization code

4735

Owner of token does not exist

User was removed from account - re-authorize

4741

Token has been revoked

Use the most recent token from latest authorization

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

Quick Reference

Flow

Grant Type

Token Expiry

Refresh

Account (S2S)

account_credentials

1 hour

Request new token

User

authorization_code

1 hour

Use refresh_token (90 day expiry)

Device

urn:ietf:params:oauth:grant-type:device_code

1 hour

Use refresh_token (90 day expiry)

Client (Chatbot)

client_credentials

1 hour

Request 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:

-

Run preflight checks firstRUNBOOK.md

-

Choose your OAuth flowconcepts/oauth-flows.md

  • 4 flows: S2S (backend), User (SaaS), Device (no browser), Chatbot
  • Decision matrix: Which flow fits your use case?

-

Understand token lifecycleconcepts/token-lifecycle.md

  • CRITICAL: How tokens expire, refresh, and revoke
  • Common pitfalls: refresh token rotation

-

Implement your flow → Jump to examples:

-

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

  • Most common OAuth error: Redirect URI mismatch

-

Implement token refreshexamples/token-refresh.md

  • Automatic middleware pattern
  • Handle refresh token rotation

-

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, User, Device, Chatbot

│   ├── 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

I want to automate Zoom tasks on my own account

I want to build a SaaS app for other Zoom users

I want to build a mobile or SPA app

  • PKCE - Why PKCE is required for public clients

I want to build an app for devices without browsers (TV, kiosk)

I'm building a Team Chat bot

I'm getting redirect URI errors (4709)

I'm getting token errors (4700-4741)

I'm getting scope errors (4711)

I need to refresh tokens

I want to understand the difference between Classic and Granular scopes

I need to secure my OAuth implementation

  • PKCE - Public client security

I want to migrate from JWT app to S2S OAuth

Note: JWT App Type was deprecated in June 2023. Migrate to S2S OAuth for server-to-server automation.

Most Critical Documents

1. OAuth Flows (DECISION DOCUMENT)

concepts/oauth-flows.md

Understand which of the 4 flows to use:

  • S2S OAuth: Backend automation (your account)
  • User OAuth: SaaS apps (users authorize you)
  • Device Flow: Devices without browsers
  • Chatbot: Team Chat bots only

2. Token Lifecycle (MOST COMMON ISSUE)

concepts/token-lifecycle.md

99% of OAuth issues stem from misunderstanding:

  • Token expiration (1 hour for all flows)
  • Refresh token rotation (must save new refresh token)
  • Revocation behavior (invalidates all tokens)

3. Redirect URI Issues (MOST COMMON ERROR)

troubleshooting/redirect-uri-issues.md

Error 4709 ("Redirect URI mismatch") is the #1 OAuth error.

Must match EXACTLY (including trailing slash, http vs https).

Key Learnings

Critical Discoveries:

-

Refresh Token Rotation

  • Each refresh returns a NEW refresh token
  • Old refresh token becomes invalid
  • Failure to save new token causes 4735 errors

-

S2S OAuth Uses Redis, User OAuth Uses Database

  • S2S: Single token for entire account → Redis (ephemeral)
  • User: Per-user tokens → Database (persistent)

-

Redirect URI Must Match EXACTLY

  • Trailing slash matters: /callback/callback/
  • Protocol matters: http://https://
  • Port matters: :3000:3001

-

PKCE Required for Public Clients

  • Mobile apps CANNOT keep secrets
  • SPAs CANNOT keep secrets
  • PKCE prevents authorization code interception

-

State Parameter Prevents CSRF

  • Generate random state before redirect
  • Store in session
  • Verify on callback

-

Token Storage Must Be Encrypted

  • NEVER store tokens in plain text
  • Use AES-256 minimum

-

JWT App Type is Deprecated (June 2023)

  • No new JWT apps can be created
  • Existing apps still work but will eventually be sunset
  • Migrate to S2S OAuth or User OAuth

-

Scope Levels Determine Authorization Requirements

  • No suffix (user-level): Any user can authorize
  • :admin: Requires admin role
  • :master: Requires account owner (multi-account)

-

Authorization Codes Expire in 5 Minutes

  • Exchange code for token immediately
  • Don't cache authorization codes

-

Device Flow Requires Polling

  • Poll at interval returned by /devicecode (usually 5s)
  • Handle authorization_pending, slow_down, expired_token

Quick Reference

"Which OAuth flow should I use?"

OAuth Flows

"Redirect URI mismatch error (4709)"

Redirect URI Issues

"Token expired or invalid"

Token Issues

"Refresh token invalid (4735)"

Token Refresh - Must save new refresh token

"Scope mismatch error (4711)"

Scope Issues

"How do I secure my OAuth app?"

PKCE + State Parameter

"How do I implement auto-refresh?"

Token Refresh

"What's the difference between Classic and Granular scopes?"

Scopes Architecture

"What error code means what?"

Common Errors

Document Version

Based on Zoom OAuth API v2 (2024+)

Deprecated: JWT App Type (June 2023)

Happy coding!

Remember: Start with OAuth Flows to understand which flow fits your use case!

Environment Variables

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card