Zoom Oauth
AnthropicOfficialCreated by ZoomDistributed by Anthropic
- 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 comprehensive documentation for Zoom API authentication. It covers the four primary OAuth authorization flows, token lifecycle management, scope configuration, and common error resolution. It serves as a technical reference for developers integrating Zoom services, offering guidance on choosing the correct grant type, implementing PKCE for public clients, and handling token refresh logic.
What it does
- Explains four Zoom OAuth flows: Server-to-Server, User Authorization, Device Authorization, and Client Authorization.
- Details token lifecycle management, including expiration, refresh, and revocation procedures.
- Provides implementation patterns for PKCE to secure public clients.
- Lists common OAuth error codes and their corresponding solutions.
- Outlines the differences between classic and granular OAuth scopes.
- Offers guidance on handling deauthorization webhooks and pre-approval requirements.
When to use it
- When determining the appropriate OAuth flow for a new Zoom integration.
- When implementing token refresh logic or handling token expiration.
- When troubleshooting specific Zoom OAuth error codes.
- When configuring app credentials, redirect URIs, or required scopes.
- When securing public-facing applications using PKCE.
Advantages
- Centralizes technical documentation for all Zoom authentication methods.
- Provides clear decision matrices for selecting the correct authorization flow.
- Includes specific troubleshooting steps for common integration failures.
- Offers structured guidance on scope management and migration.
Usage examples
- “Which Zoom OAuth flow should I use for a backend service?”
- The agent identifies the Server-to-Server (S2S) OAuth flow as the appropriate choice for backend automation without user interaction.
- “How do I fix a 4709 error in Zoom OAuth?”
- The agent explains that error 4709 indicates a redirect URI mismatch and advises verifying the URI in the Zoom Marketplace app configuration.
- “Explain the difference between classic and granular scopes.”
- The agent provides a comparison of the two scope types and links to the migration documentation for updating existing apps.
- “How do I implement PKCE for my mobile app?”
- The agent outlines the PKCE flow, including code verifier generation and the token exchange process.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/anthropics/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
Background reference for Zoom auth and token lifecycle behavior. Prefer setup-zoom-oauth first, then use this skill for the exact flow, scope, and error details.
Zoom OAuth
Authentication and authorization for Zoom APIs.
📖 Complete Documentation
For comprehensive guides, production patterns, and troubleshooting, see Integrated Index section below.
Quick navigation:
5-Minute Runbook - Preflight checks before deep debugging
OAuth Flows - Which flow to use and how each works
Token Lifecycle - Expiration, refresh, and revocation
Production Examples - Redis caching, MySQL storage, auto-refresh
Troubleshooting - Error codes 4700-4741
Prerequisites
Zoom app created in Marketplace
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_uriwith 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_uriand displayuser_codefor manual entry, ORverification_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 callbackToken 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&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
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
OAuth docs: https://developers.zoom.us/docs/integrations/oauth/
S2S OAuth docs: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
PKCE blog: https://developers.zoom.us/blog/pcke-oauth-with-postman-rest-api/
Classic scopes: https://developers.zoom.us/docs/integrations/oauth-scopes/
Granular scopes: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/
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 first → RUNBOOK.md
Choose your OAuth flow → concepts/oauth-flows.md
4 flows: S2S (backend), User (SaaS), Device (no browser), Chatbot
Decision matrix: Which flow fits your use case?
Understand token lifecycle → concepts/token-lifecycle.md
CRITICAL: How tokens expire, refresh, and revoke
Common pitfalls: refresh token rotation
Implement your flow → Jump to examples:
Backend automation → examples/s2s-oauth-redis.md
SaaS app → examples/user-oauth-mysql.md
Mobile/SPA → examples/pkce-implementation.md
Device (TV/kiosk) → examples/device-flow.md
Fix redirect URI issues → troubleshooting/redirect-uri-issues.md
Most common OAuth error: Redirect URI mismatch
Implement token refresh → examples/token-refresh.md
Automatic middleware pattern
Handle refresh token rotation
Troubleshoot errors → troubleshooting/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 referenceBy Use Case
I want to automate Zoom tasks on my own account
OAuth Flows - S2S OAuth explained
S2S OAuth Redis - Production pattern with Redis caching
Token Lifecycle - 1hr token, no refresh
I want to build a SaaS app for other Zoom users
OAuth Flows - User OAuth explained
User OAuth MySQL - Production pattern with encryption
Token Refresh - Automatic refresh middleware
Redirect URI Issues - Fix most common error
I want to build a mobile or SPA app
PKCE - Why PKCE is required for public clients
PKCE Implementation - Complete code example
State Parameter - CSRF protection
I want to build an app for devices without browsers (TV, kiosk)
OAuth Flows - Device flow explained
Device Flow Example - Complete polling implementation
Common Errors - Device-specific errors
I'm building a Team Chat bot
OAuth Flows - Chatbot flow explained
S2S OAuth Basic - Similar pattern, different grant type
Scopes Architecture - Chatbot-specific scopes
I'm getting redirect URI errors (4709)
Redirect URI Issues - START HERE!
Common Errors - Error details
User OAuth Basic - See correct pattern
I'm getting token errors (4700-4741)
Token Issues - Diagnostic workflow
Token Lifecycle - Understand expiration
Token Refresh - Implement auto-refresh
Common Errors - Error code tables
I'm getting scope errors (4711)
Scope Issues - Mismatch causes
Scopes Architecture - Classic vs Granular
Classic Scopes - Complete scope reference
Granular Scopes - Granular scope reference
I need to refresh tokens
Token Lifecycle - When to refresh
Token Refresh - Middleware pattern
Token Issues - Common mistakes
I want to understand the difference between Classic and Granular scopes
Scopes Architecture - Complete comparison
Classic Scopes -
resource:levelformatGranular Scopes -
service:action:data_claim:accessformat
I need to secure my OAuth implementation
PKCE - Public client security
State Parameter - CSRF protection
User OAuth MySQL - Token encryption at rest
I want to migrate from JWT app to S2S OAuth
S2S OAuth Redis - Modern replacement
Token Lifecycle - Different token behavior
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)
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)
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
See: Token Refresh
S2S OAuth Uses Redis, User OAuth Uses Database
S2S: Single token for entire account → Redis (ephemeral)
User: Per-user tokens → Database (persistent)
See: S2S OAuth Redis vs User OAuth MySQL
Redirect URI Must Match EXACTLY
Trailing slash matters:
/callback≠/callback/Protocol matters:
http://≠https://Port matters:
:3000≠:3001See: Redirect URI Issues
PKCE Required for Public Clients
Mobile apps CANNOT keep secrets
SPAs CANNOT keep secrets
PKCE prevents authorization code interception
See: PKCE
State Parameter Prevents CSRF
Generate random state before redirect
Store in session
Verify on callback
See: State Parameter
Token Storage Must Be Encrypted
NEVER store tokens in plain text
Use AES-256 minimum
See: User OAuth MySQL
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)See: Scopes Architecture
Authorization Codes Expire in 5 Minutes
Exchange code for token immediately
Don't cache authorization codes
See: Token Lifecycle
Device Flow Requires Polling
Poll at interval returned by
/devicecode(usually 5s)Handle
authorization_pending,slow_down,expired_tokenSee: Device Flow
Quick Reference
"Which OAuth flow should I use?"
"Redirect URI mismatch error (4709)"
"Token expired or invalid"
"Refresh token invalid (4735)"
→ Token Refresh - Must save new refresh token
"Scope mismatch error (4711)"
"How do I secure my OAuth app?"
→ PKCE + State Parameter
"How do I implement auto-refresh?"
"What's the difference between Classic and Granular scopes?"
"What error code means what?"
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
See references/environment-variables.md for standardized
.envkeys and where to find each value.
Files
- ATTRIBUTION.mdAdded by AutoGineer971 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md3.3 KiB
- SKILL.md32.2 KiB
- 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.md362.3 KiB
- references/environment-variables.md863 B
- references/granular-scopes.md368.4 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/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/authorizeToken URL:
https://zoom.us/oauth/token
Quick Decision Matrix
| Your Scenario | Flow | Grant Type |
|---|---|---|
| Backend automation on your own account | S2S OAuth | account_credentials |
| SaaS app for other Zoom users | User OAuth | authorization_code |
| Device without browser (TV, kiosk, IoT) | Device Flow | urn:ietf:params:oauth:grant-type:device_code |
| Team Chat bot only | Chatbot | client_credentials |
Two-Legged vs Three-Legged
| Type | User Involved? | Zoom Flows |
|---|---|---|
| Two-legged | No (app acts on its own) | S2S OAuth, Chatbot |
| Three-legged | Yes (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:botscopeSimpler 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
| Feature | S2S OAuth | User OAuth | Device Flow | Chatbot |
|---|---|---|---|---|
| Grant Type | account_credentials | authorization_code | device_code | client_credentials |
| User Interaction | No | Yes (browser) | Yes (separate device) | No |
| Access Token Lifetime | 1 hour | 1 hour | 1 hour | 1 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 Access | Account-wide | Per-user | Per-user | Account-wide |
| Token Storage | Redis (ephemeral) | Database (persistent) | Database (persistent) | Redis (ephemeral) |
| Use Case | Backend automation | SaaS apps | TV/kiosk apps | Chat bots |
OAuth 2.0 Standards
Zoom OAuth follows these RFCs:
RFC 6749: OAuth 2.0 Authorization Framework
RFC 7636: PKCE (Proof Key for Code Exchange)
RFC 8628: Device Authorization Grant
Next Steps
Understand token lifecycle → token-lifecycle.md
Learn about PKCE → pkce.md
Implement your flow:
Device → ../examples/device-flow.md
Requirements and compatibility
Written for: Claude Code, Claude apps
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.
Needs: Claude Code frontmatter
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
- Anthropic (Official)
- Distributed by
- Anthropic
- Source
- anthropics/knowledge-work-plugins · partner-built/zoom-plugin/skills/oauth · commit e050c2e
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from partner-built/zoom-plugin/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or Anthropic.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-oauth.zip