partner-built/zoom-plugin/skills/oauth/SKILL.md
Reference skill for Zoom authentication. Use after routing to an auth workflow when choosing app credentials, grant types, scopes, token refresh behavior, or debugging Zoom OAuth failures.
npx skillsauth add anthropics/knowledge-work-plugins zoom-oauthInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
concepts/oauth-flows.md
Understand which of the 4 flows to use:
concepts/token-lifecycle.md
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
→ Redirect URI Issues
→ Token Issues
→ Token Refresh - Must save new refresh token
→ Scope Issues
→ PKCE + State Parameter
→ Token Refresh
→ Scopes Architecture
→ 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.testing
Reads a forwarded customer email or ticket, pulls order/refund status from PayPal and account history from HubSpot, drafts a tone-matched reply in the owner's writing voice, and can issue a PayPal refund with explicit owner approval. Use when the user says "draft a response," "answer this customer," "where's my order," or "I want a refund."
development
Prepares tax-season materials for small business owners — framed as deliverables for their accountant, not tax advice. Two modes: (1) quarterly estimated tax calculation — pulls YTD net income from QuickBooks and calculates the federal income tax + self-employment tax liability and quarterly payment due; (2) year-end 1099 prep — scans QuickBooks, PayPal, and Stripe for contractors paid over $600, builds a 1099-NEC candidate list with missing W-9 flags, and produces a plain-English summary a CPA can work from directly. Trigger this skill whenever the user mentions: quarterly taxes, estimated tax payment, how much to set aside for taxes, 1099s, 1099-NEC, year-end tax prep, contractor payments, W-9s, or any phrase suggesting they are preparing for a tax deadline or handing materials to an accountant. Also trigger proactively when a user asks about net profit or YTD income in a context that suggests they are worried about their tax bill.
tools
Prepares tax-season materials — quarterly estimated tax calculation or year-end 1099 prep — and produces an accountant handoff packet. Accepts optional mode and year arguments.
tools
The front door to the Small Business plugin. Listens to what the owner needs right now — vague or specific — and routes them to the best skill or slash command for the moment. Also serves as a guide: explains what's available, suggests what to try next, and adapts recommendations based on stored business context. Trigger whenever the owner asks "what can you do," "help me with my business," "what should I focus on," "I don't know where to start," or any open-ended business request that doesn't clearly match a single skill.