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
requestsandflasklibraries (pip install requests flask) - A publicly accessible HTTPS URL for receiving webhooks (
ngrokworks 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_linkis the correct field name (notmeeting_url)video_requiredis optional. Set it toFalsefor audio + transcript only, orTrueif you need an MP4 recording- The transcription provider is configured via
recording_config.transcript.provideras a keyed object, where the provider name is the key (for example,"deepgram": { ... }) callback_urlis 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:
audio.processed— audio file is readyvideo.processed— MP4 is ready (only ifvideo_required=True)transcription.processed— transcript is readydata_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 isGET /transcript/{transcript_id}/get_transcriptand requires thetranscript_id, not thebot_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_pathPutting 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 flaskSet 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 5000Copy 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.pyYou 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. 🚀
