🚀 Executive Summary

TL;DR: Juggling Zoom Webinars and Microsoft Teams Live Events often leads to manual syncing and duplicate scheduling. This Python script automates the migration by fetching upcoming Zoom webinars and creating corresponding Teams Live Events using their respective APIs, saving significant administrative time.

🎯 Key Takeaways

  • The solution leverages Python libraries (requests, python-dotenv, msal) for secure API interactions with both Zoom and Microsoft Graph.
  • Authentication involves Zoom’s Server-to-Server OAuth flow and Microsoft Graph’s OAuth 2.0 client credentials flow, requiring specific Azure AD App Registration permissions like OnlineMeetings.ReadWrite.All.
  • Creating a Teams Live Event via the Graph API necessitates setting “isBroadcast”: True in the payload and standardizing all time data to UTC in ISO 8601 format to avoid scheduling errors.

Migrate Zoom Webinars to Microsoft Teams Live Events

Migrate Zoom Webinars to Microsoft Teams Live Events

Hey team, Darian here. A few quarters back, our Marketing department was running webinars in Zoom while the rest of the company was deep in Microsoft Teams. The result? A messy calendar, duplicate scheduling, and a weekly manual task for me to sync the two. I built this Python script to automate it, and it’s saved me a solid hour every Monday morning. If you’re juggling both platforms, this guide is for you. We’re going to pull upcoming webinars from Zoom and automatically create corresponding Live Events in Teams.

Prerequisites

  • Python 3.8 or newer installed.
  • A Zoom Pro account (or higher) with permissions to create an OAuth App for API access.
  • A Microsoft 365 account with a license that permits creating Teams Live Events.
  • An Azure AD App Registration with Graph API permissions. Specifically, you’ll need OnlineMeetings.ReadWrite.All under Application permissions.
  • Your Zoom Account ID, Client ID, and Client Secret.
  • Your Azure Tenant ID, Client ID, and Client Secret.

The Guide: Step-by-Step

Step 1: Setting Up Your Project

First, let’s get our project structure and dependencies in order. I’ll skip the standard virtual environment setup since you likely have your own workflow for that. Let’s jump straight to the Python logic.

You’ll need a few Python libraries to handle API requests and manage credentials. You can install them from your terminal: pip install requests python-dotenv msal.

Next, create a file named config.env in your project directory. This is where we’ll store our secrets. Never commit this file to version control!


# config.env
ZOOM_ACCOUNT_ID="your_zoom_account_id"
ZOOM_CLIENT_ID="your_zoom_client_id"
ZOOM_CLIENT_SECRET="your_zoom_client_secret"

AZURE_TENANT_ID="your_azure_tenant_id"
AZURE_CLIENT_ID="your_azure_client_id"
AZURE_CLIENT_SECRET="your_azure_client_secret"

# The User ID of the Teams user who will be the event organizer
TEAMS_ORGANIZER_USER_ID="user_id_of_the_organizer_in_azure_ad"

Step 2: Fetching Webinars from Zoom

Our first task is to authenticate with the Zoom API and pull a list of upcoming webinars. We’ll use Zoom’s Server-to-Server OAuth flow, which is perfect for backend services like this. The process involves getting an access token and then using that token to query the webinars endpoint.

Pro Tip: Zoom’s API documentation is quite good. I always keep a tab open to the relevant endpoint reference—in this case, the ‘List Webinars’ endpoint—to double-check expected parameters and response structures.

Here’s the Python function to handle this:


import requests
import os
from dotenv import load_dotenv

load_dotenv('config.env')

def get_zoom_webinars():
    """Fetches upcoming webinars from the Zoom API."""
    account_id = os.getenv("ZOOM_ACCOUNT_ID")
    client_id = os.getenv("ZOOM_CLIENT_ID")
    client_secret = os.getenv("ZOOM_CLIENT_SECRET")

    # First, get an access token
    auth_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={account_id}"
    auth_response = requests.post(auth_url, auth=(client_id, client_secret))
    
    if auth_response.status_code != 200:
        print(f"Error getting Zoom token: {auth_response.text}")
        return []

    access_token = auth_response.json()["access_token"]
    headers = {"Authorization": f"Bearer {access_token}"}
    
    # Now, fetch webinars for a specific user (e.g., 'me' for your own)
    # In a real-world scenario, you might iterate through a list of user IDs
    webinars_url = "https://api.zoom.us/v2/users/me/webinars"
    webinars_response = requests.get(webinars_url, headers=headers, params={"type": "upcoming"})

    if webinars_response.status_code != 200:
        print(f"Error fetching Zoom webinars: {webinars_response.text}")
        return []
        
    return webinars_response.json().get("webinars", [])

Step 3: Authenticating with Microsoft Graph

Next, we need to talk to Microsoft Teams. The gateway to all things Microsoft 365 is the Graph API. We’ll use the Microsoft Authentication Library (MSAL) for Python to handle the OAuth 2.0 client credentials flow. This is secure and designed for service-to-service communication.


import msal

def get_graph_api_token():
    """Acquires an access token for the Microsoft Graph API."""
    tenant_id = os.getenv("AZURE_TENANT_ID")
    client_id = os.getenv("AZURE_CLIENT_ID")
    client_secret = os.getenv("AZURE_CLIENT_SECRET")

    authority = f"https://login.microsoftonline.com/{tenant_id}"
    scope = ["https://graph.microsoft.com/.default"]

    app = msal.ConfidentialClientApplication(
        client_id, authority=authority, client_credential=client_secret
    )

    result = app.acquire_token_silent(scope, account=None)

    if not result:
        result = app.acquire_token_for_client(scopes=scope)

    if "access_token" in result:
        return result["access_token"]
    else:
        print(f"Error acquiring Graph token: {result.get('error_description')}")
        return None

Step 4: Creating the Teams Live Event

This is the core of our script. We’ll loop through the webinars we fetched from Zoom and create a corresponding Live Event in Teams for each one. The key is mapping the Zoom data to the JSON payload that the Graph API expects for creating an `onlineMeeting`.

Pro Tip: I use a simple text file, created_events.log, to track the Zoom webinar IDs I’ve already processed. Before creating a new event, I check this file to prevent creating duplicates every time the script runs.


import json
from datetime import datetime, timedelta

def create_teams_live_event(token, webinar):
    """Creates a Microsoft Teams Live Event based on Zoom webinar data."""
    user_id = os.getenv("TEAMS_ORGANIZER_USER_ID")
    graph_url = f"https://graph.microsoft.com/v1.0/users/{user_id}/onlineMeetings"
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    # Convert Zoom start time to ISO 8601 format required by Graph API
    start_time = datetime.strptime(webinar['start_time'], "%Y-%m-%dT%H:%M:%SZ")
    end_time = start_time + timedelta(minutes=webinar['duration'])

    payload = {
        "subject": webinar['topic'],
        "startDateTime": start_time.isoformat() + "Z",
        "endDateTime": end_time.isoformat() + "Z",
        "allowedPresenters": "everyone", # Or 'organization', 'roleIsPresenter'
        "isBroadcast": True # This makes it a Live Event instead of a regular meeting
    }
    
    response = requests.post(graph_url, headers=headers, data=json.dumps(payload))
    
    if response.status_code == 201:
        print(f"Successfully created Teams Live Event for: {webinar['topic']}")
        return response.json()
    else:
        print(f"Error creating event for '{webinar['topic']}': {response.status_code} - {response.text}")
        return None

def main():
    """Main execution function."""
    print("Starting Zoom to Teams migration script...")
    
    zoom_webinars = get_zoom_webinars()
    if not zoom_webinars:
        print("No upcoming Zoom webinars found or error occurred.")
        return

    graph_token = get_graph_api_token()
    if not graph_token:
        print("Failed to get Graph API token. Halting.")
        return

    for webinar in zoom_webinars:
        # Simple check to avoid duplicates (can be improved)
        # In production, I'd read from a log file of processed IDs
        print(f"Processing webinar: {webinar['topic']} (ID: {webinar['id']})")
        create_teams_live_event(graph_token, webinar)

if __name__ == "__main__":
    main()

Step 5: Automating the Script

To make this truly hands-off, you can schedule it to run automatically. On a Linux server, a simple cron job is perfect for this. I usually have mine run early Monday morning to sync the week’s events.

Here is an example cron entry that runs the script every Monday at 2 AM:

0 2 * * 1 python3 script.py

Just make sure your script (e.g., script.py) and the config.env file are in the location where cron executes.

Common Pitfalls (Where I Usually Mess Up)

  • Permissions, Permissions, Permissions: My biggest initial headache was Graph API permissions. If you get a 403 Forbidden error, it’s almost always because the Azure App Registration is missing the OnlineMeetings.ReadWrite.All permission or admin consent hasn’t been granted for your tenant.
  • Timezone Mismatches: The Zoom API returns times in UTC. The Microsoft Graph API also expects times in UTC, formatted in ISO 8601 (e.g., “2024-10-28T18:00:00Z”). I once spent an hour debugging events that were off by several hours. Always standardize on UTC before creating the Teams event.
  • Forgetting `isBroadcast`: If you forget to set "isBroadcast": True in the payload, the Graph API will create a regular Teams meeting, not a Live Event. This is an easy detail to miss.

Conclusion

And that’s the core of it. This script is a solid foundation that handles the most critical part of the migration. You can easily expand it to update existing events, sync registration lists, or post a notification to a Teams channel when a new event is created. The goal here is to reclaim your time by automating the tedious stuff. Hope this helps you get an hour back in your week, too.

– Darian

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How can I automate the migration of Zoom webinars to Microsoft Teams Live Events?

The provided Python script automates this by using the Zoom API to fetch upcoming webinars and the Microsoft Graph API to create corresponding Teams Live Events, handling authentication and data mapping between the platforms.

âť“ What are the alternatives to a custom Python script for Zoom to Teams migration?

Alternatives include manual scheduling, which is prone to errors and time-consuming, or using third-party integration tools. A custom Python script offers granular control, cost-effectiveness for specific needs, and direct API interaction for precise data mapping and automation.

âť“ What are common pitfalls when implementing this Zoom to Teams migration script?

Common pitfalls include insufficient Graph API permissions (e.g., missing OnlineMeetings.ReadWrite.All or admin consent), timezone mismatches requiring UTC ISO 8601 standardization, and failing to set “isBroadcast”: True in the Graph API payload, which results in a regular meeting instead of a Live Event.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading