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
- Bearer token in Authorization header (recommended):
curl -L -H "Authorization: Bearer ACCESS_TOKEN" \
"https://zoom.us/rec/archive/download/xyz"
download_access_tokenfrom 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
- Using Zoom APIs: https://developers.zoom.us/docs/api/using-zoom-apis/
- API Reference: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/