How to Build a Meeting Bot with Python in 10 Minutes

The Problem With Building Meeting Bots From Scratch

If you've ever tried to build a Zoom bot or Google Meet integration from scratch, you know how fast it turns into a maintenance nightmare. Platform SDKs change. Auth flows break. You end up spending two weeks on infrastructure that has nothing to do with the actual product you're building.

This tutorial shows you a faster path. Using MeetStream and Python, you can deploy a bot that joins a meeting, captures transcripts, audio and video, all in under 10 minutes. No platform-specific SDK wrangling. One API, every platform.

Let's build it.


What You'll Need

Before writing any code, make sure you have:

  • Python 3.8+
  • A MeetStream API key (grab one from our dashboard at app.meetstream.ai)
  • The requests and flask libraries (pip install requests flask)
  • A publicly accessible HTTPS URL for receiving webhooks (ngrok works fine for local development)
  • A Zoom or Google Meet meeting URL to test with

That's it. No OAuth dance. No platform developer accounts to configure. MeetStream handles the bot infrastructure, you just call the API.


Step 1: Authenticate With the API

MeetStream uses a token-based authentication scheme. Every request to api.meetstream.ai/api/v1 needs your API key in the Authorization header, prefixed with Token (not Bearer).

Set it up once and reuse it across all calls:

import requests

API_KEY = "your_api_key_here"
BASE_URL = "https://api.meetstream.ai/api/v1"

HEADERS = {    
  "Authorization": f"Token {API_KEY}",    
  "Content-Type": "application/json"}

Keep your API key out of source code. Use an environment variable in production:

import os

API_KEY = os.environ.get("MEETSTREAM_API_KEY")

For example

macOS / Linux:

export MEETSTREAM_API_KEY="your_api_key_here"

Windows PowerShell:

$env:MEETSTREAM_API_KEY="your_api_key_here"

Step 2: Deploy a Bot to a Meeting

This is the core call. You POST to /bots/create_bot with the meeting link, and MeetStream deploys a bot that joins the meeting and begins capturing audio and transcription.

def deploy_bot(meeting_link: str, callback_url: str, bot_name: str = "AI Notetaker") -> dict:
    payload = {
        "meeting_link": meeting_link,
        "bot_name": bot_name,
        "video_required": False,
        "callback_url": callback_url,
        "recording_config": {
            "transcript": {
                "provider": {
                    "deepgram": {
                        "language": "en",
                        "model": "nova-3"
                    }
                }
            },
            "retention": {
                "type": "timed",
                "hours": 24
            }
        }
    }

    response = requests.post(
        f"{BASE_URL}/bots/create_bot",
        json=payload,
        headers=HEADERS
    )

    response.raise_for_status()
    return response.json()


# Deploy to a Zoom meeting
result = deploy_bot(
    meeting_link="https://zoom.us/j/your-meeting-id",
    callback_url="https://your-server.com/webhooks/meetstream"
)

bot_id = result["bot_id"]
transcript_id = result["transcript_id"]

print(
    f"Bot deployed. bot_id: {bot_id}, transcript_id: {transcript_id}"
)

The response includes a bot_id and transcript_id. Store them, you'll need them later for transcript retrieval and audio/video downloads.

A few things to note about the request body

  • meeting_link is the correct field name (not meeting_url)
  • video_required is optional. Set it to False for audio + transcript only, or True if you need an MP4 recording
  • The transcription provider is configured via recording_config.transcript.provider as a keyed object, where the provider name is the key (for example, "deepgram": { ... })
  • callback_url is your webhook endpoint. MeetStream pushes lifecycle and processing events here

The same API call works for Google Meet and Microsoft Teams. Just swap the meeting link.


Step 3: Handle Webhook Events

MeetStream follows a webhook-driven architecture rather than polling. When you set a callback_url, MeetStream sends POST requests to that endpoint as the bot moves through its lifecycle.

Your server should return HTTP 200 to acknowledge each event.

Here's a Flask webhook handler covering the full lifecycle:

from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route("/webhooks/meetstream", methods=["POST"])
def handle_webhook():
    event_data = request.json
    event_type = event_data.get("event")

    if event_type == "bot.joining":
        # Bot is attempting to enter the meeting
        print(
            f"Bot joining... bot_id: {event_data.get('bot_id')}"
        )

    elif event_type == "bot.inmeeting":
        # Bot has successfully entered the active call
        print(
            f"Bot is live in the meeting. bot_id: {event_data.get('bot_id')}"
        )

    elif event_type == "bot.stopped":
        # Bot has exited — check bot_status for why
        bot_status = event_data.get("bot_status")
        print(f"Bot stopped with status: {bot_status}")

        # Normal exit: "Stopped"
        # Abnormal exits: "NotAllowed", "Denied", "Error"

    elif event_type == "transcription.processed":
        transcript_id = event_data.get("transcript_id")

        print(
            f"Transcript ready. transcript_id: {transcript_id}"
        )

        transcript = get_transcript(transcript_id)
        process_transcript(transcript)

    elif event_type == "audio.processed":
        bot_id = event_data.get("bot_id")
        print(f"Audio ready for bot_id: {bot_id}")

    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    app.run(port=5000)

Event sequence

After bot.stopped, processing events are fired in sequence:

  1. audio.processed — audio file is ready
  2. video.processed — MP4 is ready (only if video_required=True)
  3. transcription.processed — transcript is ready
  4. data_deletion — data removed according to your retention policy

Do not attempt to fetch artifacts before their corresponding event fires. Transcript generation depends on audio processing completing first.


Step 4: Retrieve the Transcript

Once transcription.processed fires, fetch the speaker-attributed transcript using the transcript_id.

def get_transcript(transcript_id: str) -> dict:
    response = requests.get(
        f"{BASE_URL}/transcript/{transcript_id}/get_transcript",
        headers=HEADERS
    )

    response.raise_for_status()
    return response.json()


def process_transcript(transcript: dict):
    segments = transcript.get("segments", [])

    for segment in segments:
        speaker = segment.get("speaker_name", "Unknown")
        text = segment.get("text", "")
        start = segment.get("start_time", 0)

        print(f"[{start:.1f}s] {speaker}: {text}")
Note: The transcript endpoint is GET /transcript/{transcript_id}/get_transcript and requires the transcript_id, not the bot_id.

Step 5: Download the Recording

If you set video_required=False, the audio MP3 becomes available after audio.processed.

If you set video_required=True, the MP4 becomes available after video.processed.

Use streaming downloads for large files rather than loading the entire response into memory.

import os


def download_audio(bot_id: str, output_path: str = None) -> str:
    url = f"{BASE_URL}/bots/{bot_id}/audio"

    response = requests.get(
        url,
        headers=HEADERS,
        stream=True
    )

    response.raise_for_status()

    output_path = output_path or f"{bot_id}_audio.mp3"

    with open(output_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

    size_mb = os.path.getsize(output_path) / (1024 * 1024)

    print(
        f"Audio saved: {output_path} ({size_mb:.1f} MB)"
    )

    return output_path


def download_video(bot_id: str, output_path: str = None) -> str:
    url = f"{BASE_URL}/bots/{bot_id}/video"

    response = requests.get(
        url,
        headers=HEADERS,
        stream=True
    )

    response.raise_for_status()

    output_path = output_path or f"{bot_id}_video.mp4"

    with open(output_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

    size_mb = os.path.getsize(output_path) / (1024 * 1024)

    print(
        f"Video saved: {output_path} ({size_mb:.1f} MB)"
    )

    return output_path

Putting It All Together

Here's the complete script, a bot deployment plus a Flask webhook server that handles the full lifecycle end to end.

import os
import requests
from flask import Flask, request, jsonify

API_KEY = os.environ.get("MEETSTREAM_API_KEY")

BASE_URL = "https://api.meetstream.ai/api/v1"

HEADERS = {
    "Authorization": f"Token {API_KEY}",
    "Content-Type": "application/json"
}

app = Flask(__name__)


# ── Bot creation ─────────────────────────────────────────────

def deploy_bot(
    meeting_link: str,
    callback_url: str,
    bot_name: str = "AI Notetaker"
) -> dict:
    payload = {
        "meeting_link": meeting_link,
        "bot_name": bot_name,
        "video_required": False,
        "callback_url": callback_url,
        "recording_config": {
            "transcript": {
                "provider": {
                    "deepgram": {
                        "language": "en",
                        "model": "nova-3"
                    }
                }
            },
            "retention": {
                "type": "timed",
                "hours": 24
            }
        }
    }

    response = requests.post(
        f"{BASE_URL}/bots/create_bot",
        json=payload,
        headers=HEADERS
    )

    response.raise_for_status()
    return response.json()


# ── Data retrieval ───────────────────────────────────────────

def get_transcript(transcript_id: str) -> dict:
    response = requests.get(
        f"{BASE_URL}/transcript/{transcript_id}/get_transcript",
        headers=HEADERS
    )

    response.raise_for_status()
    return response.json()


def download_audio(bot_id: str) -> str:
    response = requests.get(
        f"{BASE_URL}/bots/{bot_id}/audio",
        headers=HEADERS,
        stream=True
    )

    response.raise_for_status()

    path = f"{bot_id}_audio.mp3"

    with open(path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

    print(f"Audio saved: {path}")
    return path


# ── Webhook handler ──────────────────────────────────────────

@app.route("/webhooks/meetstream", methods=["POST"])
def handle_webhook():
    data = request.get_json(silent=True) or {}
    event = data.get("event")

    if event == "bot.joining":
        print(f"Bot joining: {data.get('bot_id')}")

    elif event == "bot.inmeeting":
        print(f"Bot is live: {data.get('bot_id')}")

    elif event == "bot.stopped":
        print(
            f"Bot stopped. Status: {data.get('bot_status')}"
        )

    elif event == "audio.processed":
        download_audio(data.get("bot_id"))

    elif event == "transcription.processed":
        transcript_id = data.get("transcript_id")

        transcript = get_transcript(transcript_id)

        segments = transcript.get("segments", [])

        for seg in segments:
            print(
                f"[{seg.get('start_time', 0):.1f}s] "
                f"{seg.get('speaker_name')}: "
                f"{seg.get('text')}"
            )

        # → pipe segments to your LLM,
        # store in DB, generate summary, etc.

    return jsonify({"status": "ok"}), 200


# ── Entry point ──────────────────────────────────────────────

if __name__ == "__main__":
    MEETING_URL = "https://zoom.us/j/your-meeting-id"
    WEBHOOK_URL = (
        "https://your-server.com/webhooks/meetstream"
    )

    print("Deploying bot...")

    result = deploy_bot(
        MEETING_URL,
        WEBHOOK_URL
    )

    print(
        f"Bot deployed. bot_id: {result['bot_id']}, "
        f"transcript_id: {result['transcript_id']}"
    )

    print("Starting webhook server...")
    print(
        "Make sure this endpoint is publicly reachable "
        "before deploying a bot."
    )

    app.run(port=5000)

Run it, point it at a meeting, and you have a working meeting bot. Speaker-attributed transcripts. MP3 audio. Optional MP4 video. All webhook-driven, with no polling required.

Note: In production, your webhook endpoint should already be running and publicly reachable before you deploy a bot. For simplicity, this tutorial combines bot creation and webhook handling in the same script, but they're often separate services.

Running the Bot

Install the required dependencies:

pip install requests flask

Set your MeetStream API key:

for macOS / Linux

export MEETSTREAM_API_KEY="your_api_key_here"

for Windows PowerShell

$env:MEETSTREAM_API_KEY="your_api_key_here"

Expose your local Flask server to the internet using ngrok:

ngrok http 5000

Copy the HTTPS forwarding URL generated by ngrok and use it as your webhook URL:

WEBHOOK_URL = "https://your-ngrok-url.ngrok-free.app/webhooks/meetstream"

Update the meeting URL:

MEETING_URL = "https://zoom.us/j/your-meeting-id"

Then start the script:

python bot.py

You should see output similar to:

Deploying bot...Bot deployed. bot_id: ...Starting webhook server...Bot joining...Bot is live...

Once the meeting ends and processing completes:

Audio saved: ...[12.4s] Speaker 1: Welcome everyone...[18.9s] Speaker 2: Thanks for joining...

At that point, you have a fully functioning meeting bot that can capture transcripts, recordings, and webhook events from Zoom, Google Meet, or Microsoft Teams.


What You Can Build on Top of This

The transcript and audio stream are the raw material. What you do with them is the actual product.

Developers are building things like:

AI Meeting Summaries

Pipe transcript segments into GPT-4o or Claude after transcription.processed fires. Generate summaries, action items, and follow-ups automatically.

Sales Intelligence

Detect competitor mentions, objection signals, buying intent, and customer sentiment directly from transcript data.

Real-Time Coaching

Stream live audio via live_audio_required and process it during the call rather than after it ends.

Async Briefings

Automatically generate Slack updates, meeting recaps, and task lists the moment a meeting concludes.

Compliance Recording

Store encrypted MP3 and MP4 artifacts with audit trails for regulated industries and enterprise customers.

MeetStream gives you the data layer. The intelligence layer is yours to build.


Why Not Build the Bot Infrastructure Yourself?

Fair question.

You absolutely could build directly on Zoom, Google Meet, and Microsoft Teams APIs. But each platform has its own authentication model, bot framework, rate limits, approval processes, and update cadence.

When Zoom changes its SDK, you maintain the integration.

When Google Meet changes its join flow, you update the automation.

Then there's reconnection logic, recording storage, transcript processing, monitoring, observability, security, and compliance requirements.

MeetStream absorbs that complexity so your team can focus on building the product instead of maintaining infrastructure.


Next Steps

You now have a working meeting bot in Python.

A few features worth exploring next:

Real-Time Transcription

Set live_transcription_required with a webhook URL to receive transcript segments during the meeting instead of waiting for post-processing.

Real-Time Audio Streaming

Set live_audio_required with a WebSocket URL to receive raw PCM16 audio frames per speaker while the meeting is running.

Calendar Auto-Join

Connect Google Calendar via POST /calendar/create-calendar so bots automatically join scheduled meetings.

In-Meeting Control

Set socket_connection_url to establish a WebSocket control channel for sending chat messages or playing audio through the bot.

Multi-Platform Testing

Swap the meeting link for a Google Meet or Microsoft Teams URL. The same code works without modification.


Conclusion

In less than 10 minutes, you've built a functional meeting bot that can:

  • Join Zoom, Google Meet, and Microsoft Teams meetings
  • Capture speaker-attributed transcripts
  • Download audio and video recordings
  • Receive lifecycle updates through webhooks
  • Serve as the foundation for AI-powered meeting products

The hard part isn't collecting meeting data anymore. It's deciding what to build with it.

Check out the full API documentation and start building. 🚀

Share