HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/rest-api/concepts/api-architecture.md
# 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:

```json
{
  "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

```javascript
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

```bash
# 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/[email protected]
GET /v2/users/[email protected]/meetings
```

### Common Error

Using `userId` with a User-level OAuth token:

```json
{
  "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**:

```javascript
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

```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

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

Or using UTC directly:

```json
{
  "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:

```bash
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):
```bash
curl -L -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://zoom.us/rec/archive/download/xyz"
```

2. **`download_access_token`** from webhook payload (for webhook-triggered downloads):
```bash
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:

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

const fileBuffer = await response.arrayBuffer();
```

```python
# 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:**

```json
{
  "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](https://support.zoom.us/hc/en-us/articles/4413265586189) 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](authentication-flows.md) or the **[zoom-oauth](../../oauth/SKILL.md)** 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/