Zoom API Integration (Meetings, Webinars, Recordings, Users)
Integrates the Zoom API v2 — Meetings, Webinars, Recordings, Phone, and Users management — using the zoom-python-client SDK v0.2+ with Server-to-Server OAuth authentication. When loaded, this skill makes the model implement Zoom API operations with proper token management, pagination for list endpoints, rate-limit handling, and webhook event processing.
TL;DR for Code Generation
- Initialize
ZoomApiClientfrom environment variables usingZoomApiClient.init_from_env()— never hardcode credentials - Use Server-to-Server OAuth (account_id, client_id, client_secret) — JWT tokens are deprecated by Zoom
- Handle pagination explicitly: use
next_page_tokenin list endpoint responses to iterate all pages - Wrap API calls in try/except for
requests.exceptions.HTTPErrorwith status code inspection - Respect rate limits — Zoom returns
X-RateLimit-LimitandX-RateLimit-Remainingheaders - Validate required parameters:
topic,start_time,durationfor meetings;topicfor webinars - Use ISO 8601 format (
2026-05-23T15:00:00Z) for all datetime parameters
Zoom meetings and webinars programmatically
- Listing meeting recordings and managing cloud recording retention policies
- Managing Zoom users (create, suspend, update, list) in a Zoom account
- Integrating Zoom Phone for call log retrieval, extension management, and call routing
- Processing Zoom webhook events (meeting started/ended, participant joined/left, recording completed)
- Building apps with the Zoom Apps SDK for embedded experiences within Zoom meetings
Zoom RTMS SDK (Real-Time Media Streams) instead
- Managing Zoom Rooms hardware or calendar integration — those require separate Zoom Room management APIs
Core Workflow
Authenticate with Server-to-Server OAuth: Initialize ZoomApiClient from environment variables (ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET). The SDK handles token caching and refresh automatically. Checkpoint: Verify authentication by calling zoom_client.users.get_user("me") — this returns the authenticated accounts owner details.; Identify the Target Resource: Determine user ID or email address (for user-scoped endpoints), or use the API root for account-scoped operations. User IDs can be me (the authenticated user), an email address, or the numeric user ID. Checkpoint: Confirm the user exists with get_user() before creating meetings for them.; Execute the API Operation: Call the appropriate method: create_meeting(), get_meeting(), list_recordings(), etc. Pass required parameters as keyword arguments. For list endpoints, implement pagination using next_page_token from the response. Checkpoint: Validate the response contains expected fields — for meetings, check join_url and id are present.; Handle Errors and Rate Limits: Catch requests.exceptions.HTTPError and inspect the status code: 400 (bad request / validation), 401 (bad auth / token expired), 404 (resource not found), 429 (rate limited). For 429, implement exponential backoff using the Retry-After header. Checkpoint: Log the response payload for 400s to capture validation error details.; Process Webhooks: Zoom sends event notifications to your webhook endpoint as JSON POST bodies. Validate the webhook using the Authorization header (Bearer token comparison). Acknowledge by returning HTTP 200 immediately. Checkpoint: Return 200 within 3 seconds — Zoom retries for 24 hours if you do not acknowledge.
Implementation Patterns
Pattern 1: Creating a Scheduled Meeting
import os
from datetime import datetime, timedelta
from zoom_python_client.zoom_api_client import ZoomApiClient
import requests
# ❌ BAD — JWT token (deprecated), hardcoded credentials, no error handling
import jwt
token = jwt.encode({"iss": "API_KEY", "exp": datetime.now() + timedelta(hours=1)}, "API_SECRET")
headers = {"Authorization": f"Bearer {token}"}
resp = requests.post(
"https://api.zoom.us/v2/users/me/meetings",
headers=headers,
json={"topic": "My Meeting", "type": 2, "start_time": "2026-05-23T15:00:00Z", "duration": 60},
)
print(resp.json())
# ✅ GOOD — Server-to-Server OAuth, env-based config, typed parameters, error handling
import logging
logger = logging.getLogger(__name__)
# Initialize from environment variables
# Requires: ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET
zoom_client = ZoomApiClient.init_from_env()
def create_scheduled_meeting(
topic: str,
start_time: str,
duration_minutes: int,
settings: dict | None = None,
password: str | None = None,
) -> dict:
"""Create a scheduled Zoom meeting and return meeting details."""
meeting_data: dict = {
"topic": topic,
"type": 2, # 1=instant, 2=scheduled, 3=recurring, 8=webinar
"start_time": start_time, # ISO 8601: "2026-05-23T15:00:00Z"
"duration": duration_minutes,
"timezone": "UTC",
"settings": settings or {
"host_video": True,
"participant_video": True,
"join_before_host": False,
"mute_upon_entry": True,
"approval_type": 0, # 0=auto approve, 1=manual, 2=no registration
"audio": "voip",
"auto_recording": "none",
},
}
if password:
meeting_data["password"] = password
try:
result = zoom_client.meetings.create_meeting(meeting_data)
meeting = result.json() if hasattr(result, "json") else result
logger.info("Meeting created", extra={"id": meeting.get("id"), "topic": topic, "join_url": meeting.get("join_url")})
return {
"id": meeting.get("id"),
"join_url": meeting.get("join_url"),
"start_url": meeting.get("start_url"),
"password": meeting.get("password"),
"start_time": meeting.get("start_time"),
}
except requests.exceptions.HTTPError as exc:
status = exc.response.status_code if exc.response else 0
body = exc.response.json() if exc.response and exc.response.content else {}
logger.error("Zoom meeting creation failed", extra={"status": status, "response": body})
if status == 429:
retry_after = exc.response.headers.get("Retry-After", "60")
raise RuntimeError(f"Rate limited. Retry after {retry_after}s") from exc
elif status == 401:
raise RuntimeError("Zoom authentication failed — check credentials") from exc
elif status == 404:
raise RuntimeError("User not found") from exc
raise
Pattern 2: Listing Meeting Recordings with Pagination
import os
import requests
# ❌ BAD — no pagination, only fetches first page (max 30 records)
result = zoom_client.meetings.list_meetings("me")
print(result.json())
# ✅ GOOD — full pagination, typed response, resource cleanup
def list_all_recordings(
user_id: str = "me",
page_size: int = 300,
from_date: str | None = None,
to_date: str | None = None,
) -> list[dict]:
"""List all cloud recordings for a user, handling pagination."""
all_recordings: list[dict] = []
next_page_token: str | None = None
while True:
params: dict = {
"user_id": user_id,
"page_size": min(page_size, 300),
}
if next_page_token:
params["next_page_token"] = next_page_token
if from_date:
params["from"] = from_date
if to_date:
params["to"] = to_date
try:
response = zoom_client.meetings.list_recordings(params)
data = response.json() if hasattr(response, "json") else response
meetings = data.get("meetings", [])
all_recordings.extend(meetings)
next_page_token = data.get("next_page_token")
logger.info("Fetched recordings page", extra={"count": len(meetings), "total_so_far": len(all_recordings)})
if not next_page_token:
break
except requests.exceptions.HTTPError as exc:
logger.error("Failed to list recordings", extra={"status": exc.response.status_code if exc.response else 0})
raise
return all_recordings
Pattern 3: Managing Zoom Users
import os
import requests
# ❌ BAD — no input validation, no user action filtering, no pagination
result = zoom_client.users.get_user("someone@example.com")
print(result.json())
# ✅ GOOD — user lookup with validation, action options, error handling
from typing import Literal
UserAction = Literal["activate", "deactivate", "suspend", "unsuspend"]
def get_or_create_user(email: str, first_name: str, last_name: str) -> dict:
"""Look up a Zoom user or create them if they do not exist."""
try:
result = zoom_client.users.get_user(email)
user = result.json() if hasattr(result, "json") else result
logger.info("User found", extra={"email": email, "id": user.get("id")})
return {
"id": user.get("id"),
"email": user.get("email"),
"first_name": user.get("first_name"),
"last_name": user.get("last_name"),
"status": user.get("status"),
"created": user.get("created_at"),
}
except requests.exceptions.HTTPError as exc:
if exc.response and exc.response.status_code == 404:
# User does not exist — create them
return create_user(email, first_name, last_name)
raise
def create_user(email: str, first_name: str, last_name: str) -> dict:
"""Create a new Zoom user."""
user_data = {
"action": "create", # "create", "autoCreate", "custCreate", "ssoCreate"
"user_info": {
"email": email,
"first_name": first_name,
"last_name": last_name,
"type": 1, # 1=basic (free), 2=pro, 3=corporate
},
}
try:
result = zoom_client.users.create_user(user_data)
user = result.json() if hasattr(result, "json") else result
logger.info("User created", extra={"email": email, "id": user.get("id")})
return user
except requests.exceptions.HTTPError as exc:
body = exc.response.json() if exc.response and exc.response.content else {}
logger.error("User creation failed", extra={"email": email, "response": body})
raise
def update_user_status(email: str, action: UserAction) -> bool:
"""Update a Zoom users status (suspend/unsuspend)."""
try:
zoom_client.users.update_user(email, {"action": action})
logger.info("User status updated", extra={"email": email, "action": action})
return True
except requests.exceptions.HTTPError as exc:
logger.error("Status update failed", extra={"email": email, "action": action, "status": exc.response.status_code if exc.response else 0})
return False
Constraints
MUST DO
- Use Server-to-Server OAuth (
account_id+client_id+client_secret) for authentication — Zoom deprecated JWT tokens in September 2023 - Store credentials in environment variables:
ZOOM_ACCOUNT_ID,ZOOM_CLIENT_ID,ZOOM_CLIENT_SECRET— never hardcode them - Use ISO 8601 datetime format (
2026-05-23T15:00:00Z) for allstart_timeparameters - Handle pagination on every list endpoint — Zoom returns
next_page_tokenfor responses exceedingpage_size(default 30, max 300) - Implement exponential backoff for 429 rate-limit responses — Zoom allows 1 request/second for most endpoints, 10/s for batch operations
- Validate meeting/webinar parameters before sending:
topicmax 200 chars,start_timemust be in the future,durationminimum 10 minutes
MUST NOT DO
- Use Static JWT tokens or hardcoded API Key/Secret — they are deprecated and will generate 401 responses
- Assume list endpoints return all records in one call — always paginate with
next_page_token - Hardcode Zoom user IDs — resolve them by email or use
mefor the authenticated user - Return raw Zoom API responses to end users — extract specific fields and structure the output
- Ignore webhook event validation — verify the
Authorizationheader on incoming webhooks to prevent unauthorized event injection
Output Template
When implementing Zoom API code, the output must follow this structure:
- Client Initialization —
ZoomApiClient.init_from_env()readingZOOM_ACCOUNT_ID,ZOOM_CLIENT_ID,ZOOM_CLIENT_SECRET - Resource Identification — User resolved by email or
me; meeting/webinar ID validated before operations - Parameter Construction — Dict with
topic,start_time(ISO 8601),duration(minutes),type, andsettings - API Call — Method call wrapped in try/except for
requests.exceptions.HTTPError - Pagination —
while next_page_token:loop for all list endpoints - Error Handling — Status-specific logic: 400 (validation), 401 (auth), 404 (not found), 429 (rate limit)
- Response Structuring — Extracts
id,join_url,start_url,passwordfrom responses
Zoom for meeting notification via SMS |
| coding-slack-api | Team messaging — send Zoom meeting links and recording notifications to Slack channels |
| coding-sendgrid-api | Transactional email — use for meeting confirmation and recording delivery emails |
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.