Files
2026-07-13 12:20:06 +08:00

9.1 KiB

Authentication Flows

All Zoom REST API requests require OAuth 2.0 authentication. This guide covers all supported OAuth flows and when to use each.

Complete OAuth implementation guide: See the zoom-oauth skill for full code examples, token storage, and production patterns.

Flow Selection

Flow Use Case User Interaction Token Lifetime
Server-to-Server OAuth Backend automation, bots, integrations None 1 hour
Authorization Code User-facing web apps User consent flow 1 hour (refresh: 15 years)
Authorization Code + PKCE SPAs, mobile apps User consent flow 1 hour (refresh: 15 years)
Device Code TV/IoT devices, CLI tools User enters code on separate device 1 hour (refresh: 15 years)
JWT Legacy None DEPRECATED — migrate to S2S OAuth

No user interaction required. Best for automation, scheduled tasks, and backend services.

Setup

  1. Go to Zoom App MarketplaceDevelopBuild App
  2. Select Server-to-Server OAuth
  3. Note: Account ID, Client ID, Client Secret
  4. Add required scopes (e.g., meeting:write:admin, user:read:admin)

Get Access Token

curl -X POST "https://zoom.us/oauth/token" \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=account_credentials&account_id=ACCOUNT_ID"

Response

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

Node.js — Token Manager with Auto-Refresh

class ZoomS2SAuth {
  constructor(accountId, clientId, clientSecret) {
    this.accountId = accountId;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.token = null;
    this.tokenExpiry = 0;
  }

  async getAccessToken() {
    // Return cached token if valid (with 60s buffer)
    if (this.token && Date.now() < this.tokenExpiry - 60000) {
      return this.token;
    }

    const credentials = Buffer.from(
      `${this.clientId}:${this.clientSecret}`
    ).toString('base64');

    const response = await fetch('https://zoom.us/oauth/token', {
      method: 'POST',
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: `grant_type=account_credentials&account_id=${this.accountId}`
    });

    if (!response.ok) {
      const err = await response.json();
      throw new Error(`Token error: ${err.error} - ${err.reason}`);
    }

    const data = await response.json();
    this.token = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in * 1000);

    return this.token;
  }

  async request(method, path, body = null) {
    const token = await this.getAccessToken();

    const response = await fetch(`https://api.zoom.us/v2${path}`, {
      method,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: body ? JSON.stringify(body) : undefined
    });

    if (!response.ok) {
      const err = await response.json().catch(() => ({}));
      throw new Error(`Zoom API ${response.status}: ${JSON.stringify(err)}`);
    }

    // Some endpoints return 204 No Content
    if (response.status === 204) return null;
    return response.json();
  }
}

// Usage
const zoom = new ZoomS2SAuth(
  process.env.ZOOM_ACCOUNT_ID,
  process.env.ZOOM_CLIENT_ID,
  process.env.ZOOM_CLIENT_SECRET
);

const users = await zoom.request('GET', '/users?page_size=300');
const meeting = await zoom.request('POST', '/users/user@example.com/meetings', {
  topic: 'API Meeting', type: 2, duration: 30
});

Python — Token Manager

import requests
import time
from base64 import b64encode

class ZoomS2SAuth:
    def __init__(self, account_id, client_id, client_secret):
        self.account_id = account_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.token = None
        self.token_expiry = 0

    def get_access_token(self):
        if self.token and time.time() < self.token_expiry - 60:
            return self.token

        credentials = b64encode(
            f'{self.client_id}:{self.client_secret}'.encode()
        ).decode()

        response = requests.post(
            'https://zoom.us/oauth/token',
            headers={
                'Authorization': f'Basic {credentials}',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            data=f'grant_type=account_credentials&account_id={self.account_id}'
        )
        response.raise_for_status()

        data = response.json()
        self.token = data['access_token']
        self.token_expiry = time.time() + data['expires_in']
        return self.token

    def request(self, method, path, json_data=None):
        token = self.get_access_token()
        response = requests.request(
            method,
            f'https://api.zoom.us/v2{path}',
            headers={'Authorization': f'Bearer {token}'},
            json=json_data
        )
        response.raise_for_status()
        return response.json() if response.content else None

User OAuth (Authorization Code)

For apps that act on behalf of individual Zoom users.

Flow

1. User clicks "Connect to Zoom"
2. Redirect to: https://zoom.us/oauth/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=ZZZ
3. User grants permission
4. Zoom redirects to callback: https://yourapp.com/callback?code=AUTH_CODE&state=ZZZ
5. Exchange code for tokens
6. Use access_token for API calls
7. Refresh when expired

Exchange Code for Token

curl -X POST "https://zoom.us/oauth/token" \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://yourapp.com/callback"

Refresh Token

curl -X POST "https://zoom.us/oauth/token" \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN"

Important: me Keyword

User OAuth apps must use me instead of userId in API paths:

# CORRECT for user OAuth
GET /v2/users/me/meetings

# WRONG for user OAuth — will return "Invalid access token"
GET /v2/users/abc123/meetings

Common Scopes

Scope Description
user:read Read user profile
user:read:admin Read all users (admin)
user:write:admin Manage all users (admin)
meeting:read Read meeting data
meeting:write Create/update meetings
meeting:write:admin Create/update any user's meetings
recording:read Access recordings
recording:write Manage recordings
webinar:read Read webinar data
webinar:write Manage webinars
report:read:admin View reports

Best practice: Request only the scopes you need. Fewer scopes = less user friction and faster app approval.

Token Storage Best Practices

// DO: Encrypt tokens at rest
const encrypted = encrypt(accessToken, process.env.ENCRYPTION_KEY);
await db.tokens.upsert({ userId, encrypted, expiresAt });

// DO: Use httpOnly secure cookies for web apps
res.cookie('zoom_session', sessionId, {
  httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000
});

// DON'T: Store tokens in localStorage or log them
localStorage.setItem('zoom_token', token);  // INSECURE
console.log('Token:', accessToken);          // LEAKS CREDENTIALS

Error Handling

Error Cause Solution
invalid_grant Expired/used auth code or refresh token Restart OAuth flow or re-authenticate
invalid_client Wrong client ID or secret Verify credentials
invalid_scope Scope not approved for your app Check app scopes in Marketplace
access_denied User denied permission Handle gracefully in UI
try {
  const token = await refreshAccessToken(refreshToken);
} catch (error) {
  if (error.response?.data?.error === 'invalid_grant') {
    // Refresh token revoked or expired — re-authenticate
    redirectToOAuthFlow();
  }
}

Migration from JWT (Deprecated)

The JWT app type on Zoom Marketplace is deprecated. This does not affect JWT token signatures used elsewhere (e.g., Video SDK).

Steps:

  1. Create a Server-to-Server OAuth app
  2. Request the same scopes
  3. Replace JWT token generation with OAuth token endpoint
  4. Test all endpoints
  5. Delete the JWT app

Resources