Microsoft Teams Meeting Bot: Build a Transcription Bot

Building a Microsoft Teams meeting bot with the native Teams Bot Framework requires registering an Azure app, configuring a messaging endpoint, setting up the Microsoft Graph API permissions, deploying a bot service, and submitting to the Teams App Catalog before any user can install it. For a developer who wants to add meeting transcription to a Teams workflow, this is several days of setup before writing a single line of application logic.

There is a simpler path. A Microsoft Teams meeting bot built on MeetStream requires none of that setup. You pass a Teams meeting URL when creating the bot, set a webhook URL to receive transcription events, and the bot joins the meeting and starts transcribing. No App Marketplace registration, no Azure App ID, no Graph API OAuth flow.

Teams is one of the simpler platforms to integrate via MeetStream specifically because it does not require any pre-configuration on the meeting platform side. Google Meet works the same way. Zoom requires an App Marketplace app registration (because Zoom enforces OAuth-based bot access for its meetings). But Teams meetings opened via a link accept bots without platform-side configuration, which makes the developer experience significantly faster.

In this guide, you will build a complete Teams transcription bot: create the bot with a Teams meeting link, handle the transcription.processed webhook, store speaker-labeled transcripts, and push results to a destination. Includes working curl examples and a Python FastAPI webhook handler. Let's get into it.

How MeetStream Joins Teams Meetings

When you create a bot with a Teams meeting link, MeetStream's infrastructure uses a headless browser instance to join the meeting as a participant, using the bot_name you specify as the display name. The bot appears in the participant list like any other guest. It captures audio and video from the meeting, runs transcription, and delivers the results to your webhook.

Teams meetings use WebRTC internally for media transport. MeetStream's bot participates in this WebRTC session to receive the media streams. From Teams' perspective, it is an external user joining via a meeting link. No platform-side bot registration is required because Teams allows external users to join meetings via link by default (subject to the meeting host's lobby and admission settings).

The bot will sit in the lobby if the meeting organizer has enabled lobby admission. The bot can be admitted manually, or you can configure the Teams meeting to allow all external users directly. For recurring workflows (daily standups, sales calls), setting the meeting policy to bypass the lobby for trusted participants is the cleanest solution.

Microsoft Teams transcript task module
Teams transcript task module in action via Microsoft Graph API. Source: Microsoft Learn.

Creating a Teams Bot with MeetStream

The bot creation request is a single POST to the MeetStream API. The meeting_link is the Teams meeting URL (the https://teams.microsoft.com/l/meetup-join/... format). The live_transcription_required field takes your webhook URL and MeetStream will POST transcription events to it in real time.

curl -X POST https://api.meetstream.ai/api/v1/bots/create_bot \
  -H "Authorization: Token YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "meeting_link": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_xxx%40thread.v2/0?context=%7b%22Tid%22%3a%22...",
    "bot_name": "Transcription Bot",
    "live_transcription_required": {
      "webhook_url": "https://your-server.example.com/webhook/transcription"
    }
  }'

The response includes a bot_id that you use to fetch the full transcript after the meeting ends:

# Response
{
  "bot_id": "bot_abc123xyz",
  "status": "joining",
  "meeting_link": "https://teams.microsoft.com/l/meetup-join/..."
}

If you want audio access in addition to transcription (for a custom speech model or voice agent), add live_audio_required with a WebSocket URL alongside the transcription configuration:

curl -X POST https://api.meetstream.ai/api/v1/bots/create_bot \
  -H "Authorization: Token YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "meeting_link": "https://teams.microsoft.com/l/meetup-join/...",
    "bot_name": "Transcription Bot",
    "live_transcription_required": {
      "webhook_url": "https://your-server.example.com/webhook/transcription"
    },
    "live_audio_required": {
      "websocket_url": "wss://your-server.example.com/audio"
    }
  }'

Understanding the Webhook Event Lifecycle

MeetStream sends several event types to your webhook URL as the bot moves through its state machine. Handling these correctly lets your application track meeting state and know when data is available.

Transcribe Microsoft Teams meetings
Transcription interface for Microsoft Teams meetings. Source: Transkriptor.
EventWhen it firesKey fields
bot.joiningBot is attempting to enter the meetingbot_id, meeting_link
bot.inmeetingBot has been admitted and is recordingbot_id, participants
transcription.processedEach speaker turn endsbot_id, speakerName, new_text, transcript, end_of_turn, words[]
bot.stoppedMeeting ended or bot was removedbot_id, duration_seconds
audio.processedAudio file ready for downloadbot_id, audio_url
video.processedVideo file ready for downloadbot_id, video_url

The transcription.processed event fires after each end_of_turn. The payload includes both new_text (just the current turn) and transcript (the full transcript so far). For most use cases, process only turns where end_of_turn is true and use new_text.

Python FastAPI Webhook Handler

The webhook handler receives POST requests from MeetStream, routes by event type, and processes transcription turns. The full handler stores a running transcript per bot and flushes the final record on bot.stopped.

from fastapi import FastAPI, Request
from typing import Dict, List
from datetime import datetime
import httpx
import json

app = FastAPI()

# In-memory transcript store (use Redis or a database in production)
transcripts: Dict[str, List[Dict]] = {}
MEETSTREAM_API_KEY = "YOUR_API_KEY"

@app.post("/webhook/transcription")
async def handle_webhook(request: Request):
    body = await request.json()
    event = body.get("event")
    bot_id = body.get("bot_id")

    if event == "bot.inmeeting":
        transcripts[bot_id] = []
        print(f"Bot {bot_id} entered meeting")

    elif event == "transcription.processed":
        if not body.get("end_of_turn"):
            return {"status": "partial"}

        speaker_name = body.get("speakerName", "Unknown")
        new_text = body.get("new_text", "").strip()
        words = body.get("words", [])

        if not new_text:
            return {"status": "empty"}

        turn = {
            "speaker": speaker_name,
            "text": new_text,
            "timestamp": datetime.utcnow().isoformat(),
            "words": words
        }

        if bot_id not in transcripts:
            transcripts[bot_id] = []
        transcripts[bot_id].append(turn)

        print(f"[{speaker_name}]: {new_text}")

    elif event == "bot.stopped":
        full_transcript = transcripts.get(bot_id, [])
        print(f"Meeting ended. {len(full_transcript)} turns captured.")
        await process_completed_meeting(bot_id, full_transcript)
        transcripts.pop(bot_id, None)

    return {"status": "ok"}

async def process_completed_meeting(bot_id: str, transcript: List[Dict]):
    # Fetch full transcript from API as backup/reconciliation
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://api.meetstream.ai/api/v1/transcript/{bot_id}/get_transcript",
            headers={"Authorization": f"Token {MEETSTREAM_API_KEY}"}
        )
        api_transcript = response.json()

    # Use API transcript if webhook accumulation was incomplete
    final_transcript = api_transcript if len(api_transcript) > len(transcript) else transcript

    # Push to your destination
    await save_transcript(bot_id, final_transcript)

async def save_transcript(bot_id: str, transcript: List[Dict]):
    # Write to file, push to database, call Notion API, etc.
    filename = f"/tmp/transcript_{bot_id}.json"
    with open(filename, "w") as f:
        json.dump(transcript, f, indent=2)
    print(f"Saved transcript to {filename}")

Fetching the Full Transcript After the Meeting

The webhook accumulation approach can miss turns if your server restarts, the webhook delivery fails, or the bot enters the meeting before your webhook handler is ready. Always fetch the authoritative transcript from the API after the bot.stopped event as a reconciliation step.

curl -X GET \
  https://api.meetstream.ai/api/v1/transcript/bot_abc123xyz/get_transcript \
  -H "Authorization: Token YOUR_API_KEY"

# Returns array of transcript turns:
# [
#   {"speaker": "Alice", "text": "Let's get started. Can everyone hear me?", "timestamp": "..."},
#   {"speaker": "Bob", "text": "Yes, we can hear you fine.", "timestamp": "..."},
#   ...
# ]

Structuring and Storing the Speaker-Labeled Transcript

The final transcript is a list of speaker-turn objects. Storing this in a database with speaker and timestamp indexed fields makes it queryable by speaker, by time range, and by keyword. For Teams-specific workflows, associating the bot's speaker names with Azure Active Directory user IDs enables richer analytics across meetings.

def format_transcript_as_text(transcript: List[Dict]) -> str:
    lines = []
    for turn in transcript:
        speaker = turn.get("speaker", "Unknown")
        text = turn.get("text", "")
        timestamp = turn.get("timestamp", "")
        lines.append(f"[{timestamp}] {speaker}: {text}")
    return "\n".join(lines)

def format_transcript_as_markdown(transcript: List[Dict]) -> str:
    lines = ["# Meeting Transcript\n"]
    current_speaker = None
    for turn in transcript:
        speaker = turn.get("speaker", "Unknown")
        text = turn.get("text", "")
        if speaker != current_speaker:
            lines.append(f"\n**{speaker}**")
            current_speaker = speaker
        lines.append(f"{text}")
    return "\n".join(lines)
Adding transcript bot to Teams meeting
Adding a transcription bot to a Microsoft Teams meeting. Source: Microsoft Learn.

Pushing Transcripts to Microsoft Teams Channels

After the meeting, pushing a summary or the full transcript to a Teams channel closes the loop for Teams-native workflows. The Microsoft Graph API accepts Adaptive Card messages that render structured content in a channel post. You can also push to the same Teams channel using an incoming webhook connector, which requires no Azure app registration.

import httpx

TEAMS_WEBHOOK_URL = "https://your-org.webhook.office.com/webhookb2/YOUR/CONNECTOR/ID"

async def post_transcript_to_teams_channel(
    transcript: List[Dict],
    meeting_title: str
):
    # Format a summary (truncate to 5 turns for the channel message)
    preview_turns = transcript[:5]
    preview_text = "\n".join(
        f"{t['speaker']}: {t['text']}"
        for t in preview_turns
    )
    if len(transcript) > 5:
        preview_text += f"\n... and {len(transcript) - 5} more turns"

    payload = {
        "@type": "MessageCard",
        "@context": "http://schema.org/extensions",
        "themeColor": "0076D7",
        "summary": f"Transcript: {meeting_title}",
        "sections": [{
            "activityTitle": f"Meeting Transcript: {meeting_title}",
            "activityText": preview_text,
            "facts": [
                {"name": "Turns captured", "value": str(len(transcript))},
                {"name": "Speakers", "value": str(len({t['speaker'] for t in transcript}))}
            ]
        }]
    }

    async with httpx.AsyncClient() as client:
        await client.post(TEAMS_WEBHOOK_URL, json=payload)

Conclusion

A Teams transcription bot via MeetStream is one of the faster integrations you can build. No Azure registration, no App Marketplace submission, no Graph API OAuth setup. A single API call creates the bot, a FastAPI webhook handler receives the transcription stream in real time, and the transcript endpoint delivers the full record after the meeting ends. Teams is genuinely one of the simpler platforms to start with when building meeting infrastructure. If you want to get a bot running in your next Teams call today, the MeetStream dashboard is the fastest starting point, and the API documentation covers every webhook event and endpoint.


Do I need to register an app in Microsoft Azure to build a Teams meeting bot?

Not when using MeetStream. The native Teams Bot Framework requires Azure app registration, Graph API permissions, and App Catalog submission. MeetStream's approach uses a headless browser bot that joins Teams meetings via the meeting link as an external participant, the same way a guest would join from a browser. No platform-side registration is required. You only need a MeetStream API key and the Teams meeting URL.

How does a MeetStream bot appear in a Microsoft Teams meeting?

The bot appears in the Teams participant list as a guest with the display name you set in the bot_name field. From Teams' perspective, it is an external user who joined via the meeting link. If the meeting has a lobby enabled, the bot waits there until admitted. You can configure Teams meeting policies to bypass the lobby for external users, or the meeting host can admit the bot manually from the lobby notification.

What does the transcription.processed webhook payload contain for Teams meetings?

The transcription.processed payload includes bot_id, speakerName (the Teams display name of the speaker), new_text (the text from the current speaker turn), transcript (the full transcript accumulated so far), end_of_turn (boolean), and words (an array of word-level objects with timing data). The payload format is identical across Google Meet, Zoom, and Teams. Your webhook handler does not need to be platform-aware.

How do I handle Teams meeting lobby admission for automated bots?

Teams meeting lobby settings are controlled by the meeting organizer in the meeting options. Options for automated bots are: set "Who can bypass the lobby" to "Everyone" or "People in my organization and guests", which allows the bot through automatically. If you control the calendar invite, you can configure this when creating the recurring meeting. For ad-hoc meetings where you cannot control the lobby setting, monitor the bot.joining event from MeetStream to know when the bot is waiting, and have a process to admit it from the Teams client.

Can I use MeetStream to transcribe Teams meetings that are already in progress?

Yes. You can create a bot with a Teams meeting link at any point during the meeting, not just at the start. The bot will join, wait in the lobby if needed, and begin transcribing from the moment it is admitted. The transcript will cover from the bot's admission onward, not the full meeting. If you need the complete meeting transcript including time before the bot joined, you would need to have started the bot at or before meeting start. The GET transcript endpoint always returns only what the bot captured from its admission time.

Frequently Asked Questions

Does Microsoft Teams require OAuth approval to allow bot access?

Yes. Teams bots must be registered in Azure Active Directory and granted the OnlineMeeting.ReadBasic.All and Calls.AccessMedia.All permissions. For production deployments, your app must pass Microsoft's app review before it can join meetings in external tenants.

How does a Teams bot join a meeting without a Teams account?

You use the Microsoft Graph Calls API to programmatically join a meeting by passing the meeting join URL. The bot authenticates via a service principal and receives audio/video media through the Real-Time Media Platform SDK, which handles the media negotiation.

What is the latency profile for Teams transcription bots?

End-to-end latency from audio capture to transcript delivery typically runs 800ms to 2 seconds, depending on the STT provider. The Teams Real-Time Media Platform introduces about 150ms of its own buffering, which is unavoidable at the platform level.

How do I handle the bot being ejected from a Teams meeting?

Subscribe to the call state change webhook in the Graph API. When the bot receives a terminated or disconnected event, your service should log the disconnect reason, attempt one reconnect after 5 seconds, and mark the session as failed if the reconnect does not succeed within 30 seconds.