Generated banner for Zoom Meeting SDK Linux
Generated logo for Zoom Meeting SDK Linux

Zoom Meeting SDK Linux

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk

From the author

Zoom Meeting SDK for Linux - C++ headless meeting bots with raw audio/video access, transcription, recording, and AI integration for server-side automation
— the description by Zoom

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Zoom Meeting SDK for Linux provides a C++ library designed for headless server environments, enabling developers to build bots that participate in Zoom meetings. It supports raw audio and video data access, allowing for custom transcription, recording, and AI-powered meeting analysis. The SDK is optimized for server-side automation and requires specific system dependencies, including PulseAudio for audio handling and GLib for event loop management.

What it does

  • Enables headless bot participation in Zoom meetings
  • Provides access to raw PCM audio and YUV420 video data
  • Supports local raw recording and cloud recording control
  • Facilitates integration with AI pipelines for real-time transcription and analysis
  • Handles meeting events through an asynchronous GLib event loop

When to use it

  • When building a bot that must join a meeting as a participant to capture media
  • When you need to feed raw audio or video directly into an AI processing pipeline
  • When developing automated meeting assistants or quality monitoring tools
  • When deploying meeting bots in server environments without a graphical user interface

Advantages

  • Optimized for headless server operation
  • Direct access to raw media streams for custom processing
  • Comprehensive support for meeting lifecycle management
  • Compatible with standard Linux distributions like Ubuntu and CentOS

Usage examples

“How do I start raw recording in my C++ bot?”
The bot must first call StartRawRecording() from the meeting recording controller, ensuring it has host, co-host, or token-based permissions.
“What is required to get audio working in a headless environment?”
You must install pulseaudio and pulseaudio-utils, create a configuration file at ~/.config/zoomus.conf, and start the service with virtual sinks.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/zoom-meeting-sdk-linux/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip zoom-meeting-sdk-linux.zip -d .agents/skills

Documentation by Zoom

Zoom Meeting SDK - Linux Development

Expert guidance for building headless meeting bots with the Zoom Meeting SDK on Linux. This SDK enables server-side meeting participation, raw media capture, transcription, and AI-powered meeting automation.

How to Build a Meeting Bot That Automatically Joins and Records

Use this skill when the requirement is:

  • visible bot joins a real Zoom meeting

  • the bot records raw media itself

  • or the bot triggers a Zoom-managed cloud-recording workflow after join

Skill chain:

  • primary: meeting-sdk/linux

  • add zoom-rest-api for OBF/ZAK lookup, scheduling, or cloud-recording settings

  • add zoom-webhooks when post-meeting cloud recording retrieval is required

Minimal raw-recording flow:

JoinParam join_param;
join_param.userType = SDK_UT_WITHOUT_LOGIN;
auto& params = join_param.param.withoutloginuserJoin;
params.meetingNumber = meeting_number;
params.userName = "Recording Bot";
params.psw = meeting_password.c_str();
params.app_privilege_token = obf_token.c_str();

SDKError join_err = meeting_service->Join(join_param);
if (join_err != SDKERR_SUCCESS) {
    throw std::runtime_error("join_failed");
}

// In MEETING_STATUS_INMEETING callback:
auto* record_ctrl = meeting_service->GetMeetingRecordingController();
if (!record_ctrl) {
    throw std::runtime_error("recording_controller_unavailable");
}

if (record_ctrl->CanStartRawRecording() != SDKERR_SUCCESS) {
    throw std::runtime_error("raw_recording_not_permitted");
}

SDKError record_err = record_ctrl->StartRawRecording();
if (record_err != SDKERR_SUCCESS) {
    throw std::runtime_error("start_raw_recording_failed");
}

GetAudioRawdataHelper()->subscribe(new MyAudioDelegate());

Use raw recording when the bot must own PCM/YUV media or feed an AI pipeline directly. Use cloud recording + webhooks when the requirement is Zoom-managed MP4/M4A/transcript assets after the meeting.

Official Documentation: https://developers.zoom.us/docs/meeting-sdk/linux/ API Reference: https://marketplacefront.zoom.us/sdk/meeting/linux/ Sample Repository (Raw Recording): https://github.com/zoom/meetingsdk-linux-raw-recording-sample Sample Repository (Headless): https://github.com/zoom/meetingsdk-headless-linux-sample

New to Meeting SDK Linux? Follow this path:

  1. linux.md - Quick start guide with complete workflow

  2. concepts/high-level-scenarios.md - Production bot architectures

  3. meeting-sdk-bot.md - Resilient bot with retry logic

  4. references/linux-reference.md - Dependencies, Docker, CMake

Common Use Cases:

Having issues?

Routing Rule for Bots

If the user asks to build a bot that automatically joins a Zoom meeting and records it, start with meeting-sdk-bot.md.

  • Use Meeting SDK Linux for the visible participant, join flow, and raw recording control.

  • Chain zoom-rest-api when the bot must fetch OBF/ZAK tokens, schedule meetings, or enable account-side recording settings.

  • Chain zoom-webhooks when the requirement is Zoom cloud recording retrieval after meeting end.

SDK Overview

The Zoom Meeting SDK for Linux is a C++ library optimized for headless server environments:

  • Headless Operation: No GUI required, perfect for Docker/cloud

  • Raw Data Access: YUV420 video, PCM audio at 32kHz

  • GLib Event Loop: Async event handling for callbacks

  • Docker-Ready: Pre-configured Dockerfiles for CentOS/Ubuntu

  • PulseAudio Integration: Virtual audio devices for headless environments

Key Differences from Video SDK
FeatureMeeting SDK (Linux)Video SDK
Primary UseJoin existing meetings as botHost custom video sessions
VisibilityVisible participantSession participant
UIHeadless (no UI)Optional custom UI
AuthenticationJWT + OBF/ZAK for external meetingsJWT only
Recording ControlStartRawRecording() requiredDirect raw data access
PlatformLinux onlyWindows, macOS, iOS, Android

Prerequisites

System Requirements
  • OS: Ubuntu 22+, CentOS 8/9, Oracle Linux 8

  • Architecture: x86_64

  • Compiler: gcc/g++ with C++11 support

  • Build Tools: cmake 3.16+

Development Dependencies
# Ubuntu
apt-get install -y build-essential cmake \
    libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \
    libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \
    libglib2.0-dev libcurl4-openssl-dev pulseaudio

# CentOS
yum install -y cmake gcc gcc-c++ \
    libxcb-devel xcb-util-image xcb-util-keysyms \
    glib2-devel libcurl-devel pulseaudio
Required Credentials
  1. Zoom Meeting SDK App (Client ID & Secret) → Create at Marketplace

  2. JWT Token → Generate from Client ID/Secret

  3. For External Meetings: OBF token OR ZAK token → Get via REST API

  4. For Raw Recording: Meeting Recording Token (optional) → Get via API

Quick Start

1. Download & Extract SDK
# Download from https://marketplace.zoom.us/
tar xzf zoom-meeting-sdk-linux_x86_64-{version}.tar

# Organize files
mkdir -p demo/include/h demo/lib/zoom_meeting_sdk
cp -r h/* demo/include/h/
cp lib*.so demo/lib/zoom_meeting_sdk/
cp -r qt_libs demo/lib/zoom_meeting_sdk/
cp translation.json demo/lib/zoom_meeting_sdk/json/

# Create required symlink
cd demo/lib/zoom_meeting_sdk && ln -s libmeetingsdk.so libmeetingsdk.so.1
2. Initialize & Auth
#include "zoom_sdk.h"

USING_ZOOM_SDK_NAMESPACE

// Initialize SDK
InitParam init_params;
init_params.strWebDomain = "https://zoom.us";
init_params.enableLogByDefault = true;
init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap;
InitSDK(init_params);

// Authenticate with JWT
AuthContext auth_ctx;
auth_ctx.jwt_token = your_jwt_token;
CreateAuthService(&auth_service);
auth_service->SDKAuth(auth_ctx);
3. Join Meeting
// In onAuthenticationReturn callback
void onAuthenticationReturn(AuthResult ret) {
    if (ret == AUTHRET_SUCCESS) {
        JoinParam join_param;
        join_param.userType = SDK_UT_WITHOUT_LOGIN;

        auto& params = join_param.param.withoutloginuserJoin;
        params.meetingNumber = 1234567890;
        params.userName = "Bot";
        params.psw = "password";
        params.isVideoOff = true;
        params.isAudioOff = false;

        meeting_service->Join(join_param);
    }
}
4. Access Raw Data
// In onMeetingStatusChanged callback
void onMeetingStatusChanged(MeetingStatus status, int iResult) {
    if (status == MEETING_STATUS_INMEETING) {
        auto* record_ctrl = meeting_service->GetMeetingRecordingController();

        // Start raw recording (enables raw data access)
        if (record_ctrl->CanStartRawRecording() == SDKERR_SUCCESS) {
            record_ctrl->StartRawRecording();

            // Subscribe to audio
            auto* audio_helper = GetAudioRawdataHelper();
            audio_helper->subscribe(new MyAudioDelegate());

            // Subscribe to video
            IZoomSDKRenderer* video_renderer;
            createRenderer(&video_renderer, new MyVideoDelegate());
            video_renderer->setRawDataResolution(ZoomSDKResolution_720P);
            video_renderer->subscribe(user_id, RAW_DATA_TYPE_VIDEO);
        }
    }
}

Key Features

FeatureDescription
Headless OperationNo GUI, perfect for Docker/server deployments
Raw Audio (PCM)Capture mixed or per-user audio at 32kHz
Raw Video (YUV420)Capture video frames in contiguous planar format
GLib Event LoopAsync callback handling
Docker SupportPre-built Dockerfiles for CentOS/Ubuntu
PulseAudio Virtual DevicesAudio in headless environments
Breakout RoomsProgrammatic breakout room management
ChatSend/receive in-meeting chat
Recording ControlLocal, cloud, and raw recording

Critical Gotchas & Best Practices

⚠️ CRITICAL: PulseAudio for Docker/Headless

The #1 issue for raw audio in Docker:

Raw audio requires PulseAudio and a config file, even in headless environments.

Solution:

# Install PulseAudio
apt-get install -y pulseaudio pulseaudio-utils

# Create config file
mkdir -p ~/.config
cat > ~/.config/zoomus.conf << EOF
[General]
system.audio.type=default
EOF

# Start PulseAudio with virtual devices
pulseaudio --start --exit-idle-time=-1
pactl load-module module-null-sink sink_name=virtual_speaker
pactl load-module module-null-sink sink_name=virtual_mic

See: references/linux-reference.md#pulseaudio-setup

Raw Recording Permission Required

Unlike Video SDK, Meeting SDK requires explicit permission to access raw data:

// MUST call StartRawRecording() first
auto* record_ctrl = meeting_service->GetMeetingRecordingController();

SDKError can_record = record_ctrl->CanStartRawRecording();
if (can_record == SDKERR_SUCCESS) {
    record_ctrl->StartRawRecording();
    // NOW you can subscribe to audio/video
} else {
    // Need: host/co-host status OR recording token
}

Ways to get permission:

  1. Bot is host/co-host

  2. Host grants recording permission

  3. Use recording_token parameter (get via REST API)

  4. Use app_privilege_token (OBF) when joining

GLib Main Loop Required

SDK callbacks execute via GLib event loop:

#include <glib.h>

// Setup main loop
GMainLoop* loop = g_main_loop_new(NULL, FALSE);

// Add timeout for periodic tasks
g_timeout_add_seconds(10, check_meeting_status, NULL);

// Run loop (blocks until quit)
g_main_loop_run(loop);

Without GLib loop: Callbacks never fire, join hangs indefinitely.

Always use heap mode for raw data to avoid stack overflow with large frames:

init_params.rawdataOpts.videoRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap;
init_params.rawdataOpts.shareRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap;
init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap;
Thread Safety

SDK callbacks execute on SDK threads:

  • Don't perform heavy operations in callbacks

  • Don't call CleanUPSDK() from within callbacks

  • Use thread-safe queues for data passing

  • Use mutexes for shared state

Production Architectures

Transcription Bot

See: concepts/high-level-scenarios.md#scenario-1

Join Meeting → StartRawRecording → Subscribe Audio →
Stream to AssemblyAI/Whisper → Generate Real-time Transcript
Recording Bot

See: concepts/high-level-scenarios.md#scenario-2

Join Meeting → StartRawRecording → Subscribe Audio+Video →
Write YUV+PCM → FFmpeg Merge → Upload to Storage
AI Meeting Assistant

See: concepts/high-level-scenarios.md#scenario-3

Join Meeting → Real-time Transcription → AI Analysis →
Extract Action Items → Generate Summary

Sample Applications

Official Repositories:

SampleDescriptionLink
Raw Recording SampleTraditional C++ approach with config.txtGitHub
Headless SampleModern TOML-based with CLI, Docker ComposeGitHub

What Each Demonstrates:

  • Raw Recording Sample: Complete raw data workflow, PulseAudio setup, Docker multi-distro support

  • Headless Sample: AssemblyAI integration, LLM analysis, production-ready config management

Documentation Library

Core Concepts
Platform Reference
Authentication
  • ../references/authorization.md - SDK JWT generation

  • ../references/bot-authentication.md - Bot token types (ZAK, OBF, JWT)

Advanced Features
  • ../references/breakout-rooms.md - Programmatic breakout rooms

  • ../references/ai-companion.md - AI Companion controls

Common Issues

IssueCauseSolution
No audio in DockerMissing PulseAudio configCreate ~/.config/zoomus.conf
Raw recording deniedNo permissionUse host/co-host OR recording token
Callbacks not firingMissing GLib main loopAdd g_main_loop_run()
Build errors (XCB)Missing X11 librariesInstall libxcb packages
Segfault on authOpenSSL version mismatchInstall libssl1.1

Complete troubleshooting: references/linux-reference.md#troubleshooting

Resources


Need help? Start with linux.md for quick start, then explore high-level-scenarios.md for production patterns.

Operations

  • RUNBOOK.md - 5-minute preflight and debugging checklist.

Files

meeting-sdk-bot.md

Meeting SDK Bot (Linux)

Build resilient meeting bots that join Zoom meetings as visible participants using the Meeting SDK.

Overview

Meeting SDK bots join as real participants (visible in participant list) and can access raw audio/video data for recording, transcription, or AI processing.

Use this approach when:

  • You need to be visible in the participant list

  • You want to interact with meeting features (chat, reactions, etc.)

  • You need local recording control

  • You're processing your own meetings or have user consent

Alternative: See rtms/examples/rtms-bot.md for invisible, read-only access via RTMS.

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                    MEETING SDK BOT FLOW                              │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ 1. Pre-Join: REST API                                               │
│    └── Get meeting schedule (number, password, start time)          │
│    └── Get OBF token for user (bot joins "on behalf of" user)       │
└────────────────────────────┬────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 2. Join with Retry (OBF requires owner present)                     │
│    └── Retry with configurable interval until owner joins           │
│    └── Circuit breaker: Stop after N attempts                       │
└────────────────────────────┬────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 3. Start Raw Recording (Meeting SDK singleton)                      │
│    └── IMeetingRecordingController::StartRawRecording()             │
│    └── Subscribe to raw audio/video                                 │
└────────────────────────────┬────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 4. Process Media Streams                                            │
│    └── Audio: PCM data via IZoomSDKAudioRawDataDelegate             │
│    └── Video: YUV420 frames via IZoomSDKVideoRawDataDelegate        │
└────────────────────────────┬────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 5. Mid-Meeting: Connection Monitoring                               │
│    └── Detect disconnections → Exponential backoff retry            │
│    └── Stop after N reconnection attempts                           │
└─────────────────────────────────────────────────────────────────────┘

Skills Required

SkillPurpose
zoom-rest-apiGet meeting schedule, retrieve OBF token
zoom-meeting-sdk (Linux)Join meeting, control recording, access raw media

Choose the Recording Mode

GoalPrimary pathSkills
Bot writes its own audio/video filesStartRawRecording() + raw audio/video delegateszoom-meeting-sdk (Linux)
Zoom-hosted MP4/M4A/transcript assets after meeting endMeeting/account cloud recording settings + recording.completed webhook + recordings download APIzoom-rest-api + zoom-webhooks

Use raw recording when the bot must process or persist media itself. Use the cloud-recording path when the requirement is post-meeting retrieval of Zoom-managed recording assets.

Automatic Join + Recording Flow

zoom-rest-api
  -> get meeting metadata
  -> get OBF or ZAK token
Meeting SDK Linux bot
  -> join with retry
  -> CanStartRawRecording()
  -> StartRawRecording()
  -> subscribe raw audio/video
  -> write PCM/YUV or forward to AI pipeline
Optional post-meeting cloud path
  -> zoom-webhooks recording.completed
  -> zoom-rest-api recordings download

Prerequisites

  • Zoom Meeting SDK v5.15+ (Linux)

  • Meeting SDK JWT credentials (SDK Key + Secret)

  • Server-to-Server OAuth app or OAuth app for REST API access

  • REST API scopes: meeting:read, user:read, optionally meeting:write if triggering meetings

  • Raw Data entitlement enabled (Admin → Account Settings → Meeting → "Allow access to raw data")

Configuration

Retry Parameters (Customizable)
// config.h or environment variables
struct BotConfig {
    // Join retry (waiting for owner to be present)
    int join_retry_attempts = 5;          // Max join attempts (default: 5)
    int join_retry_interval_ms = 60000;   // Constant interval: 60s (default: 1min)

    // Mid-meeting reconnection (network failures)
    int reconnect_max_attempts = 3;       // Max reconnection attempts (default: 3)
    int reconnect_base_delay_ms = 2000;   // Initial delay: 2s (default: 2s)
    // Exponential backoff: 2s, 4s, 8s...

    // Meeting schedule polling (if webhook unavailable)
    int schedule_poll_interval_ms = 30000; // Poll every 30s (default: 30s)

    // Timeout settings
    int auth_timeout_ms = 10000;          // SDK auth timeout (default: 10s)
    int join_timeout_ms = 30000;          // Single join attempt timeout (default: 30s)
};

// Load from environment variables (recommended for production)
BotConfig loadConfig() {
    BotConfig cfg;

    // Override defaults from env vars if present
    const char* env;

    if ((env = getenv("BOT_JOIN_RETRY_ATTEMPTS"))) {
        cfg.join_retry_attempts = atoi(env);
    }
    if ((env = getenv("BOT_JOIN_RETRY_INTERVAL_MS"))) {
        cfg.join_retry_interval_ms = atoi(env);
    }
    if ((env = getenv("BOT_RECONNECT_MAX_ATTEMPTS"))) {
        cfg.reconnect_max_attempts = atoi(env);
    }
    if ((env = getenv("BOT_RECONNECT_BASE_DELAY_MS"))) {
        cfg.reconnect_base_delay_ms = atoi(env);
    }

    return cfg;
}
Customization Guide
ParameterDefaultWhen to IncreaseWhen to Decrease
join_retry_attempts5High-priority meetings, owner often lateTesting, short-lived meetings
join_retry_interval_ms60000 (1min)Meetings with long pre-join bufferNeed faster failure detection
reconnect_max_attempts3Unstable networks, critical meetingsBatch processing, cost-sensitive
reconnect_base_delay_ms2000 (2s)Network latency high (international)Local network, low latency

Recommended Ranges:

  • Join retry attempts: 3-10

  • Join retry interval: 30s-5min

  • Reconnect attempts: 2-5

  • Reconnect base delay: 1s-5s

Examples:

# High-priority production bot (aggressive retries)
export BOT_JOIN_RETRY_ATTEMPTS=10
export BOT_JOIN_RETRY_INTERVAL_MS=30000  # 30s
export BOT_RECONNECT_MAX_ATTEMPTS=5
export BOT_RECONNECT_BASE_DELAY_MS=1000  # 1s

# Cost-sensitive batch processing (conservative)
export BOT_JOIN_RETRY_ATTEMPTS=3
export BOT_JOIN_RETRY_INTERVAL_MS=120000  # 2min
export BOT_RECONNECT_MAX_ATTEMPTS=2
export BOT_RECONNECT_BASE_DELAY_MS=5000   # 5s

# Development/testing (fail fast)
export BOT_JOIN_RETRY_ATTEMPTS=2
export BOT_JOIN_RETRY_INTERVAL_MS=10000   # 10s
export BOT_RECONNECT_MAX_ATTEMPTS=1
export BOT_RECONNECT_BASE_DELAY_MS=1000   # 1s

Step 1: Get Meeting Info + OBF Token (REST API)

Get Meeting Schedule
# Get meeting details
curl "https://api.zoom.us/v2/meetings/{meetingId}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
  "id": 1234567890,
  "topic": "Team Standup",
  "start_time": "2026-02-09T15:00:00Z",
  "password": "abc123",
  "pmi": false
}

Store: meeting number, password, start time.

Get OBF Token

The bot joins "on behalf of" a Zoom user. Get the user's OBF token:

# Get OBF token for user
curl -X POST "https://api.zoom.us/v2/users/{userId}/token?type=obf&ttl=7200" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
  "token": "eyJhbGc...",
  "expire_in": 7200
}

CRITICAL: The bot cannot join until the owner (the user whose OBF token you're using) is present in the meeting. This is why retry logic is essential.

Error if REST API fails: ABORT. No meeting info = cannot proceed.

Step 2: Join Meeting with Retry Logic

Join Implementation
#include <chrono>
#include <thread>

class MeetingBot {
private:
    BotConfig config;
    IMeetingService* meetingService;
    bool joinSuccessful = false;
    bool ownerNotPresentError = false;

public:
    // Attempt to join with retry logic
    bool joinMeetingWithRetry(
        uint64_t meetingNumber,
        const string& password,
        const string& obfToken,
        const string& botDisplayName
    ) {
        for (int attempt = 1; attempt <= config.join_retry_attempts; attempt++) {
            cout << "[JOIN] Attempt " << attempt << "/"
                 << config.join_retry_attempts << endl;

            // Attempt join
            JoinParam joinParam;
            joinParam.userType = SDK_UT_WITHOUT_LOGIN;

            JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin;
            param.meetingNumber = meetingNumber;
            param.userName = botDisplayName.c_str();
            param.psw = password.c_str();
            param.isVideoOff = true;   // Bots typically don't send video
            param.isAudioOff = false;  // Need audio for transcription
            param.app_privilege_token = obfToken.c_str();  // OBF token

            SDKError err = meetingService->Join(joinParam);

            if (err != SDKERR_SUCCESS) {
                cerr << "[JOIN] Join() call failed: " << err << endl;

                // Check if it's a retriable error
                if (shouldRetryJoin(err)) {
                    if (attempt < config.join_retry_attempts) {
                        cout << "[JOIN] Retrying in "
                             << (config.join_retry_interval_ms / 1000)
                             << "s..." << endl;
                        this_thread::sleep_for(
                            chrono::milliseconds(config.join_retry_interval_ms)
                        );
                        continue;
                    }
                }

                // Non-retriable error or out of attempts
                cerr << "[JOIN] Giving up after " << attempt << " attempts" << endl;
                return false;
            }

            // Join() call succeeded, wait for callback
            if (waitForJoinCallback()) {
                cout << "[JOIN] Successfully joined meeting!" << endl;
                return true;
            } else {
                cerr << "[JOIN] Join callback indicated failure" << endl;

                // If owner not present, retry
                if (ownerNotPresentError && attempt < config.join_retry_attempts) {
                    cout << "[JOIN] Owner not present. Retrying in "
                         << (config.join_retry_interval_ms / 1000) << "s..." << endl;
                    this_thread::sleep_for(
                        chrono::milliseconds(config.join_retry_interval_ms)
                    );
                    continue;
                }
            }
        }

        cerr << "[JOIN] Failed to join after "
             << config.join_retry_attempts << " attempts" << endl;
        return false;
    }

private:
    bool shouldRetryJoin(SDKError err) {
        switch (err) {
            case SDKERR_WRONG_USAGE:
            case SDKERR_INVALID_PARAMETER:
            case SDKERR_MODULE_LOAD_FAILED:
                return false;  // Configuration errors, don't retry

            case SDKERR_NETWORK_ERROR:
            case SDKERR_SERVICE_FAILED:
                return true;   // Network/server issues, retry

            default:
                return true;   // Unknown error, retry to be safe
        }
    }

    bool waitForJoinCallback() {
        // Wait for onMeetingStatusChanged callback
        // Implementation depends on your callback handling
        // Typical: use condition variable with timeout

        std::unique_lock<std::mutex> lock(callbackMutex);
        bool success = callbackCV.wait_for(
            lock,
            std::chrono::milliseconds(config.join_timeout_ms),
            [this]{ return joinSuccessful || ownerNotPresentError; }
        );

        return success && joinSuccessful;
    }

public:
    // Callback from SDK
    void onMeetingStatusChanged(MeetingStatus status, int iResult) {
        std::lock_guard<std::mutex> lock(callbackMutex);

        if (status == MEETING_STATUS_INMEETING) {
            joinSuccessful = true;
            callbackCV.notify_one();
        } else if (status == MEETING_STATUS_FAILED) {
            // Check error code for "owner not present"
            if (iResult == MEETING_FAIL_OBF_OWNER_NOT_IN_MEETING) {
                ownerNotPresentError = true;
            }
            joinSuccessful = false;
            callbackCV.notify_one();
        }
    }

private:
    std::mutex callbackMutex;
    std::condition_variable callbackCV;
};
Common Join Errors
Error CodeMeaningAction
MEETING_FAIL_OBF_OWNER_NOT_IN_MEETINGOBF token owner not in meeting yetRETRY (owner might join soon)
MEETING_FAIL_MEETING_NOT_EXISTMeeting not startedRETRY if before end time
MEETING_FAIL_INCORRECT_MEETING_NUMBERWrong meeting IDABORT (config error)
MEETING_FAIL_MEETING_NOT_STARTMeeting hasn't startedRETRY until start time
MEETING_FAIL_INVALID_TOKENOBF token invalid/expiredABORT (need new token)

Step 3: Start Raw Recording

Once joined, request permission to access raw audio/video:

void onMeetingStatusChanged(MeetingStatus status, int iResult) {
    if (status == MEETING_STATUS_INMEETING) {
        cout << "[BOT] Joined successfully, starting raw recording..." << endl;

        // Get recording controller
        IMeetingRecordingController* recordCtrl =
            meetingService->GetMeetingRecordingController();

        if (!recordCtrl) {
            cerr << "[BOT] Failed to get recording controller" << endl;
            return;
        }

        // Check permission
        SDKError canRecord = recordCtrl->CanStartRawRecording();
        if (canRecord != SDKERR_SUCCESS) {
            cerr << "[BOT] Cannot start raw recording: " << canRecord << endl;
            cerr << "[BOT] Check: Raw Data entitlement enabled in Admin settings?" << endl;
            return;
        }

        // Start raw recording (enables raw data flow)
        SDKError err = recordCtrl->StartRawRecording();
        if (err != SDKERR_SUCCESS) {
            cerr << "[BOT] StartRawRecording failed: " << err << endl;
            return;
        }

        cout << "[BOT] Raw recording started, subscribing to media..." << endl;

        // Subscribe to audio/video
        subscribeToRawMedia();
    }
}

IMPORTANT: StartRawRecording() does NOT create a file. It enables access to raw audio/video data streams.

Manage Recording Lifecycle Explicitly

The bot should treat raw recording as a capability switch plus media subscriptions:

class RecordingSession {
public:
    void start(IMeetingService* meetingService) {
        auto* recordCtrl = meetingService->GetMeetingRecordingController();
        if (!recordCtrl) {
            throw std::runtime_error("recording_controller_unavailable");
        }

        SDKError canRecord = recordCtrl->CanStartRawRecording();
        if (canRecord != SDKERR_SUCCESS) {
            throw std::runtime_error("raw_recording_not_permitted");
        }

        SDKError err = recordCtrl->StartRawRecording();
        if (err != SDKERR_SUCCESS) {
            throw std::runtime_error("start_raw_recording_failed");
        }

        audioHelper = GetAudioRawdataHelper();
        audioHelper->subscribe(&audioDelegate, true);

        createRenderer(&videoRenderer, &videoDelegate);
        videoRenderer->setRawDataResolution(ZoomSDKResolution_720P);
        videoRenderer->subscribe(activeUserId, RAW_DATA_TYPE_VIDEO);
    }

    void stop(IMeetingService* meetingService) {
        if (audioHelper) {
            audioHelper->unSubscribe();
        }
        if (videoRenderer) {
            videoRenderer->unSubscribe();
        }

        auto* recordCtrl = meetingService->GetMeetingRecordingController();
        if (recordCtrl) {
            recordCtrl->StopRawRecording();
        }
    }

private:
    IZoomSDKAudioRawDataHelper* audioHelper = nullptr;
    IZoomSDKRenderer* videoRenderer = nullptr;
    MyAudioDelegate audioDelegate;
    MyVideoDelegate videoDelegate;
    uint32_t activeUserId = 0;
};

Persisting a recording is your job after raw data arrives. Typical outputs are:

  • PCM audio -> WAV/FLAC encoder or streaming transcription pipeline

  • YUV420 video -> FFmpeg transcode to MP4 or frame-by-frame CV pipeline

  • Mixed bot pipeline -> raw capture first, then post-process after leave

Step 4: Subscribe to Raw Audio/Video

void subscribeToRawMedia() {
    // Subscribe to raw audio
    IZoomSDKAudioRawDataDelegate* audioDelegate = new MyAudioDelegate();
    SDKError audioErr = meetingService->GetMeetingAudioController()
        ->GetMeetingAudioHelper()
        ->subscribe(audioDelegate, true);  // true = mixed audio

    if (audioErr != SDKERR_SUCCESS) {
        cerr << "[AUDIO] Subscribe failed: " << audioErr << endl;
    } else {
        cout << "[AUDIO] Subscribed to mixed audio" << endl;
    }

    // Subscribe to raw video
    IZoomSDKVideoRawDataDelegate* videoDelegate = new MyVideoDelegate();
    SDKError videoErr = meetingService->GetMeetingVideoController()
        ->GetMeetingVideoHelper()
        ->subscribe(videoDelegate);

    if (videoErr != SDKERR_SUCCESS) {
        cerr << "[VIDEO] Subscribe failed: " << videoErr << endl;
    } else {
        cout << "[VIDEO] Subscribed to video streams" << endl;
    }
}
Cloud Recording Alternative

If the requirement is Zoom-managed cloud recording instead of raw media capture, use Meeting SDK only for the joining bot and use API/webhook skills for the recording workflow:

curl -X POST "https://api.zoom.us/v2/users/{userId}/meetings" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "Bot Recorded Meeting",
    "type": 2,
    "start_time": "2026-03-06T18:00:00Z",
    "settings": {
      "auto_recording": "cloud"
    }
  }'

Then subscribe to recording.completed and download assets through the recordings APIs:

  • zoom-webhooks -> receive recording.completed

  • zoom-rest-api -> GET /meetings/{meetingId}/recordings or GET /users/{userId}/recordings

Use this path when the desired output is Zoom-hosted MP4/M4A/transcript files rather than bot-owned raw PCM/YUV.

Step 5: Mid-Meeting Reconnection

Connection Monitoring
class MeetingBot {
private:
    BotConfig config;
    int reconnectionAttempt = 0;

public:
    void onMeetingStatusChanged(MeetingStatus status, int iResult) {
        switch (status) {
            case MEETING_STATUS_RECONNECTING:
                cout << "[BOT] Connection lost, SDK is reconnecting..." << endl;
                break;

            case MEETING_STATUS_FAILED:
            case MEETING_STATUS_DISCONNECTING:
                handleDisconnection(iResult);
                break;

            case MEETING_STATUS_INMEETING:
                // Reconnection successful
                if (reconnectionAttempt > 0) {
                    cout << "[BOT] Reconnected successfully!" << endl;
                    reconnectionAttempt = 0;  // Reset counter
                }
                break;
        }
    }

private:
    void handleDisconnection(int errorCode) {
        reconnectionAttempt++;

        cout << "[RECONNECT] Disconnected (error: " << errorCode
             << "), attempt " << reconnectionAttempt << "/"
             << config.reconnect_max_attempts << endl;

        if (reconnectionAttempt >= config.reconnect_max_attempts) {
            cerr << "[RECONNECT] Giving up after "
                 << reconnectionAttempt << " attempts" << endl;
            cleanup();
            notifyFailure("Max reconnection attempts exceeded");
            return;
        }

        // Exponential backoff: 2s, 4s, 8s...
        int delay_ms = config.reconnect_base_delay_ms
                       * (1 << (reconnectionAttempt - 1));

        cout << "[RECONNECT] Retrying in " << (delay_ms / 1000) << "s..." << endl;

        // Schedule reconnection
        std::thread([this, delay_ms]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
            attemptRejoin();
        }).detach();
    }

    void attemptRejoin() {
        // Re-use same meeting number, password, OBF token
        // If OBF token expired, fetch new one from REST API first

        if (isOBFTokenExpired()) {
            cout << "[RECONNECT] OBF token expired, fetching new one..." << endl;
            // TODO: Call REST API to get fresh OBF token
        }

        // Call join again
        bool success = joinMeetingWithRetry(
            cachedMeetingNumber,
            cachedPassword,
            cachedOBFToken,
            cachedBotName
        );

        if (!success) {
            cerr << "[RECONNECT] Rejoin failed" << endl;
            cleanup();
            notifyFailure("Reconnection failed");
        }
    }
};
Customizing Reconnection Behavior
// Example: Linear backoff instead of exponential
int delay_ms = config.reconnect_base_delay_ms * reconnectionAttempt;

// Example: Capped exponential backoff (max 30s)
int delay_ms = std::min(
    config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1)),
    30000  // Cap at 30s
);

// Example: Jittered backoff (avoid thundering herd)
int base_delay = config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1));
int jitter = rand() % 1000;  // Random 0-1000ms
int delay_ms = base_delay + jitter;

Recording Fallback: Local vs Cloud

If raw recording fails, fall back to local or cloud recording:

void startRecordingWithFallback() {
    IMeetingRecordingController* ctrl =
        meetingService->GetMeetingRecordingController();

    // Try raw recording first
    if (ctrl->CanStartRawRecording() == SDKERR_SUCCESS) {
        SDKError err = ctrl->StartRawRecording();
        if (err == SDKERR_SUCCESS) {
            cout << "[RECORDING] Using raw recording" << endl;
            return;
        }
    }

    // Fallback: Local recording
    if (ctrl->CanStartRecording(true) == SDKERR_SUCCESS) {
        SDKError err = ctrl->StartRecording(
            chrono::system_clock::now(),
            "/tmp/bot_recording.mp4"
        );
        if (err == SDKERR_SUCCESS) {
            cout << "[RECORDING] Using local recording" << endl;
            return;
        }
    }

    // Fallback: Cloud recording
    if (ctrl->CanStartCloudRecording() == SDKERR_SUCCESS) {
        SDKError err = ctrl->StartCloudRecording();
        if (err == SDKERR_SUCCESS) {
            cout << "[RECORDING] Using cloud recording" << endl;
            return;
        }
    }

    cerr << "[RECORDING] All recording methods failed" << endl;
}

Complete Resilient Bot Example

int main() {
    // 1. Load configuration
    BotConfig config = loadConfig();

    // 2. Initialize Meeting SDK
    InitParam initParam;
    initParam.strWebDomain = "https://zoom.us";
    initParam.emLanguageID = LANGUAGE_English;
    initParam.enableLogByDefault = true;

    SDKError err = InitSDK(initParam);
    if (err != SDKERR_SUCCESS) {
        cerr << "InitSDK failed: " << err << endl;
        return 1;
    }

    // 3. Authenticate SDK with JWT
    AuthContext authCtx;
    authCtx.jwt_token = generateJWT(SDK_KEY, SDK_SECRET);

    IAuthService* authService = CreateAuthService();
    authService->SDKAuth(authCtx);

    // Wait for auth callback...

    // 4. Fetch meeting info + OBF token from REST API
    MeetingInfo meetingInfo = fetchMeetingInfoFromAPI(MEETING_ID);
    string obfToken = fetchOBFTokenFromAPI(USER_ID);

    if (meetingInfo.empty() || obfToken.empty()) {
        cerr << "ABORT: Failed to get meeting info or OBF token" << endl;
        return 1;
    }

    // 5. Join meeting with retry
    MeetingBot bot(config);
    bool joined = bot.joinMeetingWithRetry(
        meetingInfo.number,
        meetingInfo.password,
        obfToken,
        "Transcription Bot"
    );

    if (!joined) {
        cerr << "ABORT: Failed to join meeting" << endl;
        return 1;
    }

    // 6. SDK callbacks handle: StartRawRecording, subscribe, reconnection

    // 7. Keep running until meeting ends
    bot.runEventLoop();

    // 8. Cleanup
    bot.cleanup();
    CleanUPSDK();

    return 0;
}

Comparison: Meeting SDK Bot vs RTMS Bot

AspectMeeting SDK BotRTMS Bot
VisibilityVisible participantInvisible (read-only service)
AuthenticationJWT + OBF tokenREST API trigger + webhook
Join DependencyOwner must be presentNo dependency on participants
Retry LogicRequired (owner presence)Not applicable (webhook-based)
Media AccessRaw audio/video/share via SDKAudio/video/text/share/chat via WebSocket
Recording ControlFull (local, cloud, raw)None (read-only)
InteractionCan send chat, reactionsCannot interact
Resource UsageHigher (full SDK)Lower (WebSocket only)
Use CaseInteractive bots, recording, moderationPassive transcription, analytics

Choose Meeting SDK Bot when:

  • You need to interact with the meeting (chat, reactions)

  • You need local recording control

  • You want to be visible in participant list

  • You're processing your own meetings

Choose RTMS Bot when:

  • You only need to observe/transcribe

  • You want minimal resource usage

  • You prefer invisible operation

  • You're processing external meetings

Troubleshooting

Bot Never Joins (OBF Owner Not Present)

Symptom: All join attempts fail with "owner not in meeting"

Solution:

  1. Verify owner (OBF token holder) has joined the meeting

  2. Increase join_retry_attempts or join_retry_interval_ms

  3. Check meeting start time - don't attempt join before meeting starts

  4. Consider webhook: Listen for meeting.participant_joined event for owner

Raw Recording Permission Denied

Symptom: CanStartRawRecording() returns error

Solution:

  1. Admin → Account Settings → Meeting → In Meeting (Advanced)

  2. Enable "Allow access to raw data (audio, video, sharing) for Meeting SDK"

  3. Requires "Raw Data" entitlement from Zoom support

Frequent Disconnections During Meeting

Symptom: Bot reconnects multiple times, then gives up

Solution:

  1. Increase reconnect_max_attempts (e.g., 5 instead of 3)

  2. Increase reconnect_base_delay_ms if network is slow

  3. Check server network stability

  4. Monitor CPU/memory usage (insufficient resources can cause disconnects)

OBF Token Expires During Meeting

Symptom: Reconnection fails with "invalid token"

Solution:

  1. Fetch fresh OBF token in attemptRejoin() before rejoining

  2. Monitor token expiration (expire_in from API response)

  3. Request longer-lived tokens (max TTL: 7200s = 2 hours)

Resources

Requirements and compatibility

Written for: OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Zoom
Publisher
OpenAI (Official)
Distributed by
OpenAI
Source
openai/plugins · plugins/zoom/skills/meeting-sdk/linux · commit d420d13
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-meeting-sdk-linux.zip