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

8.9 KiB

API Architecture

Core design patterns for the Zoom REST API — base URLs, regional routing, identifiers, time formats, and request conventions.

Base URL

All requests use HTTPS with API version /v2 in the path:

https://api.zoom.us/v2/

GraphQL uses a separate versioned endpoint:

https://api.zoom.us/v3/graphql

Regional Base URLs

The OAuth token response includes an api_url field indicating the user's data region. Use this for data residency compliance:

{
  "access_token": "eyJ...",
  "api_url": "https://api-eu.zoom.us"
}

Construct your regional base URL by appending /v2/:

Region API URL Base URL
Global (default) https://api.zoom.us https://api.zoom.us/v2
Australia https://api-au.zoom.us https://api-au.zoom.us/v2
Canada https://api-ca.zoom.us https://api-ca.zoom.us/v2
European Union https://api-eu.zoom.us https://api-eu.zoom.us/v2
India https://api-in.zoom.us https://api-in.zoom.us/v2
Saudi Arabia https://api-sa.zoom.us https://api-sa.zoom.us/v2
Singapore https://api-sg.zoom.us https://api-sg.zoom.us/v2
United Kingdom https://api-uk.zoom.us https://api-uk.zoom.us/v2
United States https://api-us.zoom.us https://api-us.zoom.us/v2
Vanity account https://{vanity}.zoom.us https://{vanity}.zoom.us/v2

Important: The global URL https://api.zoom.us always works regardless of user region. Regional URLs are for compliance, not required.

Node.js — Dynamic Base URL from Token

async function getZoomClient(accountId, clientId, clientSecret) {
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

  const tokenRes = 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=${accountId}`
  });

  const tokenData = await tokenRes.json();
  const baseUrl = tokenData.api_url
    ? `${tokenData.api_url}/v2`
    : 'https://api.zoom.us/v2';

  return {
    accessToken: tokenData.access_token,
    baseUrl,
    async request(method, path, body = null) {
      const res = await fetch(`${this.baseUrl}${path}`, {
        method,
        headers: {
          'Authorization': `Bearer ${this.accessToken}`,
          'Content-Type': 'application/json'
        },
        body: body ? JSON.stringify(body) : undefined
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(`Zoom API ${res.status}: ${err.message}`);
      }
      return res.json();
    }
  };
}

The me Keyword

The me keyword substitutes for userId or accountId in API paths. Its behavior varies by app type:

App Type me Behavior When to Use
User-level OAuth Resolves to the authenticated user MUST use — providing userId causes invalid token error
Server-to-Server OAuth Not supported MUST NOT use — provide actual userId or email
Account-level OAuth Resolves to the user who installed the app Can use either me or userId

Examples

# User OAuth app — MUST use me
GET /v2/users/me
GET /v2/users/me/meetings

# S2S OAuth app — MUST use actual userId or email
GET /v2/users/abc123def
GET /v2/users/john@example.com
GET /v2/users/john@example.com/meetings

Common Error

Using userId with a User-level OAuth token:

{
  "code": 4700,
  "message": "Invalid access token, does not contain scopes."
}

Fix: Replace the userId with me.

Meeting ID vs UUID

  • Meeting ID: Numeric identifier for the meeting. Reusable for recurring meetings. Expires 30 days after last use.
  • UUID: Unique identifier for a specific meeting instance. Never expires. Generated per occurrence of recurring meetings.

When to Use Which

Use Case Use
Get a scheduled meeting Meeting ID
Get a past meeting instance UUID
Get recordings for a specific session UUID
Report on a specific occurrence UUID

Double-Encoding UUIDs

UUIDs that begin with / or contain // must be double URL-encoded:

function encodeUUID(uuid) {
  // Check if double-encoding is needed
  if (uuid.startsWith('/') || uuid.includes('//')) {
    return encodeURIComponent(encodeURIComponent(uuid));
  }
  return encodeURIComponent(uuid);
}

// UUID: /abcABC123==
// Single encode: %2FabcABC123%3D%3D
// Double encode: %252FabcABC123%253D%253D  ← Required

const meetingUUID = '/abcABC123==';
const url = `https://api.zoom.us/v2/past_meetings/${encodeUUID(meetingUUID)}`;

Python

from urllib.parse import quote

def encode_uuid(uuid_str):
    if uuid_str.startswith('/') or '//' in uuid_str:
        return quote(quote(uuid_str, safe=''), safe='')
    return quote(uuid_str, safe='')

uuid = '/abcABC123=='
url = f'https://api.zoom.us/v2/past_meetings/{encode_uuid(uuid)}'

Time Formats

Zoom API uses ISO 8601 with two variants:

Format Meaning Example
yyyy-MM-ddTHH:mm:ssZ UTC time (Z suffix) 2025-03-15T10:00:00Z
yyyy-MM-ddTHH:mm:ss Local time (no Z, uses timezone field) 2025-03-15T10:00:00

Setting Meeting Time

{
  "topic": "Team Meeting",
  "type": 2,
  "start_time": "2025-03-15T10:00:00",
  "timezone": "America/Los_Angeles",
  "duration": 60
}

Or using UTC directly:

{
  "topic": "Team Meeting",
  "type": 2,
  "start_time": "2025-03-15T17:00:00Z",
  "duration": 60
}

Note: Some Report APIs only accept UTC format. Always check the endpoint reference for the accepted format.

Date-Only Parameters

Some endpoints (e.g., recordings list) use YYYY-MM-DD format:

GET /v2/users/me/recordings?from=2025-01-01&to=2025-01-31

Download URLs

Recording download_url values in API responses and webhook payloads are dynamically generated. They require authentication:

Authentication Methods

  1. Bearer token in Authorization header (recommended):
curl -L -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://zoom.us/rec/archive/download/xyz"
  1. download_access_token from webhook payload (for webhook-triggered downloads):
curl -L -H "Authorization: Bearer DOWNLOAD_ACCESS_TOKEN" \
  "https://zoom.us/rec/archive/download/xyz"

Follow Redirects

Download URLs may return HTTP 301/302 redirects. Always follow redirects:

// Node.js — fetch follows redirects by default
const response = await fetch(downloadUrl, {
  headers: { 'Authorization': `Bearer ${accessToken}` },
  redirect: 'follow'
});

const fileBuffer = await response.arrayBuffer();
# Python — requests follows redirects by default
import requests

response = requests.get(
    download_url,
    headers={'Authorization': f'Bearer {access_token}'},
    allow_redirects=True,
    stream=True
)

with open('recording.mp4', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)

Personal Meeting ID (PMI)

Users can create meetings with their PMI. The API returns a unique meeting ID in the response, but webhook events still reference the PMI. Use the PMI when passing IDs to API endpoints for PMI-based meetings.

Shared Access Permissions

Users with Schedule Privilege or role-based access can act on behalf of other users. If your app accesses resources of a user other than the one who installed the app, that user must have authorized shared access permissions.

Error when shared access is not granted:

{
  "code": 403,
  "message": "authenticated user has not permitted access to the targeted resource"
}

Resolution: Direct the user to enable shared access permissions in their Zoom settings. See Zoom Help Center for the user-facing instructions.

Email Address Display Rules

External participant emails are only shown if:

  • The participant entered their email during registration
  • The host provided the email via calendar integration, authentication exception, or breakout room assignment
  • A CSV was imported for webinar panelists/attendees

High API Failure Rates

If your app has a consistently high error-to-request ratio, Zoom may disable it. Build robust error handling and graceful retry logic.

Request Authentication

All API requests require a Bearer token in the Authorization header:

Authorization: Bearer {access_token}

Full auth implementation: See Authentication Flows or the zoom-oauth skill.

Resources