From zoom-skills
Guides Zoom OAuth authentication across four flows: Account S2S, User authorization code, Device, Client credentials. Manages access/refresh tokens, errors for API integration.
npx claudepluginhub zoom/skills --plugin zoom-skillsThis skill uses the workspace's default tool permissions.
Authentication and authorization for Zoom APIs.
RUNBOOK.mdconcepts/oauth-flows.mdconcepts/pkce.mdconcepts/scopes-architecture.mdconcepts/state-parameter.mdconcepts/token-lifecycle.mdexamples/device-flow.mdexamples/pkce-implementation.mdexamples/s2s-oauth-basic.mdexamples/s2s-oauth-redis.mdexamples/token-refresh.mdexamples/user-oauth-basic.mdexamples/user-oauth-mysql.mdreferences/classic-scopes.mdreferences/environment-variables.mdreferences/granular-scopes.mdreferences/oauth-errors.mdtroubleshooting/common-errors.mdtroubleshooting/redirect-uri-issues.mdtroubleshooting/scope-issues.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Validates product 'why' before building via diagnostics, founder reviews of codebases, user journey audits, and ICE-scored feature prioritization. Outputs briefs and go/no-go recs.
Authentication and authorization for Zoom APIs.
For comprehensive guides, production patterns, and troubleshooting, see Integrated Index section below.
Quick navigation:
| 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) |
| 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" |
βββββββββββββββββββββββ
β 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 β β β
βββββββββββββββββββββββββββ βββββββββββββββββββ
For backend automation without user interaction.
POST https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ACCOUNT_ID}
Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "user:read:user:admin",
"api_url": "https://api.zoom.us"
}
Access tokens expire after 1 hour. No separate refresh flow - just request a new token.
For apps that act on behalf of users.
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) |
redirect_uri with authorization code:
https://example.com/?code={AUTHORIZATION_CODE}
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.
{
"access_token": "eyJ...",
"token_type": "bearer",
"refresh_token": "eyJ...",
"expires_in": 3600,
"scope": "user:read:user",
"api_url": "https://api.zoom.us"
}
POST https://zoom.us/oauth/token?grant_type=refresh_token&refresh_token={REFRESH_TOKEN}
Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
| 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) |
For devices without browsers (e.g., Meeting SDK apps).
Enable "Use App on Device" in: Features > Embed > Enable Meeting SDK
POST https://zoom.us/oauth/devicecode?client_id={CLIENT_ID}
Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
{
"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
}
Direct user to:
verification_uri and display user_code for manual entry, ORverification_uri_complete (user code prefilled)User signs in and allows the app.
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)}
{
"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"
}
| 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 |
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
}
}
}
Same as User Authorization. If refresh token expires, restart device flow from Step 1.
For chatbot message operations only.
POST https://zoom.us/oauth/token?grant_type=client_credentials
Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "imchat:bot",
"api_url": "https://api.zoom.us"
}
Tokens expire after 1 hour. No refresh flow - just request a new token.
GET https://api.zoom.us/v2/users/me
Headers:
Authorization: Bearer {ACCESS_TOKEN}
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 |
Works for all authorization types.
POST https://zoom.us/oauth/revoke?token={ACCESS_TOKEN}
Headers:
Authorization: Basic {Base64(ClientID:ClientSecret)}
{
"status": "success"
}
For public clients that can't securely store secrets (mobile apps, SPAs, desktop apps).
| 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 |
ββββββββββββ ββββββββββββ ββββββββββββ
β 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 ββββββββ β
β β β
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
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)}
When a user removes your app, Zoom sends a webhook to your Deauthorization Notification Endpoint URL.
{
"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"
}
}
Some Zoom accounts require Marketplace admin pre-approval before users can authorize apps.
In-meeting feature showing apps with real-time access to content.
| Type | Description | For |
|---|---|---|
| Classic scopes | Legacy scopes (user, admin, master levels) | Existing apps |
| Granular scopes | New fine-grained scopes with optional support | New apps |
For previously-created apps. Three levels:
Full list: https://developers.zoom.us/docs/integrations/oauth-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/
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}
Notes:
| 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.
| 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 |
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.
This section was migrated from SKILL.md.
If you're new to Zoom OAuth, follow this order:
Run preflight checks first β RUNBOOK.md
Choose your OAuth flow β concepts/oauth-flows.md
Understand token lifecycle β concepts/token-lifecycle.md
Implement your flow β Jump to examples:
Fix redirect URI issues β troubleshooting/redirect-uri-issues.md
Implement token refresh β examples/token-refresh.md
Troubleshoot errors β troubleshooting/common-errors.md
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
resource:level formatservice:action:data_claim:access formatNote: JWT App Type was deprecated in June 2023. Migrate to S2S OAuth for server-to-server automation.
Understand which of the 4 flows to use:
99% of OAuth issues stem from misunderstanding:
troubleshooting/redirect-uri-issues.md
Error 4709 ("Redirect URI mismatch") is the #1 OAuth error. Must match EXACTLY (including trailing slash, http vs https).
Refresh Token Rotation
S2S OAuth Uses Redis, User OAuth Uses Database
Redirect URI Must Match EXACTLY
/callback β /callback/http:// β https://:3000 β :3001PKCE Required for Public Clients
State Parameter Prevents CSRF
Token Storage Must Be Encrypted
JWT App Type is Deprecated (June 2023)
Scope Levels Determine Authorization Requirements
:admin: Requires admin role:master: Requires account owner (multi-account)Authorization Codes Expire in 5 Minutes
Device Flow Requires Polling
/devicecode (usually 5s)authorization_pending, slow_down, expired_tokenβ OAuth Flows
β Token Issues
β Token Refresh - Must save new refresh token
β Scope Issues
β PKCE + State Parameter
β Token Refresh
β Common Errors
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!
.env keys and where to find each value.