How to Put a Voice AI Agent in Google Meet, Zoom or Teams
How to make an AI agent speak in a meeting without talking over people: wake words, end_of_turn, barge-in with the interrupt command, echo, latency, and how Meet, Zoom and Teams admit a bot.
To put a voice AI agent into a Google Meet, Zoom or Teams call, send a meeting bot into the call that streams the room's audio to your agent and plays its speech back as a named participant. The hard part is turn-taking: when may the agent speak, and how does it stop?
MeetStream is a meeting bot API for AI agents: one API call sends a bot into Zoom, Google Meet or Microsoft Teams that can hear the room, speak and act, and stream audio and transcripts live. This guide covers the turn-taking layer on top of it.
Updated October 2026. Written by Navaneeth (Nav) Jawahar, CTO of MeetStream, which has run about 80,000 meeting bots and processed over a million meeting minutes. It draws on building MeetStream's voice agents and debugging agents in real group calls.
Why is a voice agent in a meeting harder than a one-to-one voice call?
A one-to-one voice agent has one human and a simple rule: when the human stops, the agent talks. A meeting breaks that rule. Several people share the floor, most of what they say is not meant for the agent, and a pause after one person often means someone else is about to speak.
- Who is it for? The agent has to work out whether a question was aimed at it. Answering a question meant for a colleague is the fastest way to get a bot removed.
- Who speaks next? When one person finishes, two others may start at once. An agent that grabs every gap talks over people.
- Crosstalk. Two people can talk at once. MeetStream's live audio stream tags each frame with the dominant speaker, and the live transcript names a speaker on every event.
- Room manners. People accept a short wait from an agent they addressed. They do not accept one that volunteers opinions or keeps talking after someone says "hold on".
Per-participant audio (on Zoom, Google Meet and Microsoft Teams; set audio_separate_streams to true) helps attribution in post-call analysis. For live turn-taking, work from the live audio stream plus the live transcript, which names a speaker on every event.
Should the agent use a wake word, always listen, or wait to be addressed?
In a group call, start with a wake word or an address by name, and keep always-listening for one-to-one calls. The deciding factor is who pays for a mistake. A missed trigger costs one repeated question; a false trigger interrupts the whole room.
| Activation mode | How the agent decides to speak | Typical failure | Use it when |
|---|---|---|---|
| Wake word | A fixed phrase such as "Hey Atlas" appears in a finished turn | Misheard phrase; people forget it | Large or mixed meetings, and your first release |
| Addressed by name | A small classifier decides whether each finished turn was a request to the agent | Answers passing mentions ("Atlas would know") | Small team calls where people talk to it naturally |
| Always listening | Responds to every finished turn | Talks over people, answers questions meant for others | One-to-one calls, mock interviews, a facilitator role |
| Chat only | Listens, then answers in the meeting chat | Easy to miss in a busy call | Large meetings, or when a voice would distract |
For addressed-by-name, give the classifier the last two or three finished turns, the agent's name and the participant list, and ask a yes or no question: was the latest turn a request to the agent? Run it only on finished turns, or it will fire on half a sentence.
For a wake word, match on the transcript, not raw audio, and accept mishearings: speech-to-text may write "Hey Atlas" as "hey, Atlas" or "a atlas". Then open a short listening window so the follow-up does not need the phrase again. MIA, MeetStream's hosted voice agent, has wake words built in.
How does the agent know a person has finished speaking?
Use the end-of-turn signal from your streaming transcript, then check that nobody else has taken the floor. In MeetStream live transcripts, every event carries an end_of_turn flag and every word carries word_is_final, so interim words are drafts and you act only on finished turns.
Set live_transcription_required.webhook_url on create_bot and pick a streaming provider under recording_config.transcript.provider, such as deepgram_streaming or assemblyai_streaming. Each event includes speakerName, new_text, the running transcript, a words array with timings, end_of_turn and your custom_attributes. The live transcription guide shows a full payload.
How quickly a turn ends is a provider setting, and defaults differ a lot. Deepgram's endpointing waits for 10 ms of silence by default. AssemblyAI's turn detection ends a turn when model confidence passes 0.4 after at least 400 ms of silence, and forces one at 1,280 ms. OpenAI Realtime offers silence-based server_vad and a semantic_vad with an eagerness setting (OpenAI VAD guide), and Gemini Live has automatic activity detection with tunable sensitivity and silence duration (Gemini Live guide).
Silence alone is weak in meetings, because people pause mid-thought. A semantic end-of-turn model, or a check that the text is a complete question, cuts false starts. Then add a floor check: between end_of_turn and your first audio chunk, watch the live audio. If a different person is already speaking, hold the answer, and drop it if the talk has moved on.
What latency does conversational turn-taking need?
People leave gaps of about 200 ms between turns, far less than the 600 ms or more it takes to plan even one word, because listeners predict the end of a turn and prepare early. An agent cannot match that, so aim to start speaking soon after a clear end of turn and never leave the room wondering if it heard.
Levinson and Torreira's review of turn-taking timing puts typical gaps near 200 ms, single-word planning near 600 ms, and about 1,500 ms before a person starts an unprepared sentence. A 10-language study by Stivers and colleagues found the most common gap in every language fell between 0 and 200 ms.
A reasonable target is to start the agent's audio within about 1.5 seconds of the turn that addressed it. That is not a standard; it is roughly how long a person takes to start an unplanned sentence. If a tool call will take longer, say a short "Let me check" first. The time goes in four places:
- Meeting audio to your server: MeetStream's live audio runs at about 200 ms.
- End-of-turn detection: from 10 ms to over a second, depending on provider settings.
- Model time to first audio: first token plus text-to-speech (TTS), or a speech model's first frame.
- Audio back into the meeting: your first
sendaudiochunk.
Log a timestamp at each hop before you tune anything.
How do you handle barge-in when someone talks over the agent?
Barge-in is a person talking while the agent speaks. The agent should stop sending audio, tell the bot to drop what it has queued, and tell its model how much of the answer people heard.
On MeetStream, that means you stop sending sendaudio chunks on the control WebSocket (socket_connection_url) and send interrupt with "action": "clear_audio_queue". The control command reference lists every control command. Keep only about a second of audio queued at the bot, so the agent stops within about a second.
Detect barge-in on audio, not the transcript, which arrives later. A good first rule: frames tagged with a person's name, above a speech energy threshold, for about 300 ms. That skips coughs and most "mm-hm" sounds. MIA ships its own interruption handling, so you only tune this in your own pipeline.
With OpenAI Realtime over WebSocket, the conversation guide says to stop playback on input_audio_buffer.speech_started and send conversation.item.truncate with audio_end_ms, so the model remembers only what the room heard. Gemini Live cancels the generation itself and sets serverContent.interrupted.
Here is the speaking half of a bring-your-own agent in Python. It creates the bot, accepts both WebSockets, streams model audio in half-second chunks with about a second of lead, and barges in when a person speaks over it.
import asyncio, base64, json, os
import numpy as np, requests
from websockets.asyncio.server import serve
API = "https://api.meetstream.ai/api/v1"
BOT_NAME = "Atlas (AI assistant)"
RATE, CHUNK_S, LEAD_S = 48000, 0.5, 1.0 # 48 kHz mono, 0.5 s chunks, ~1 s queued
SPEECH_RMS, HOLD_S = 500, 0.3 # barge-in: this loud for this long
def create_bot(meeting_link, public_wss):
r = requests.post(f"{API}/bots/create_bot",
headers={"Authorization": f"Token {os.environ['MEETSTREAM_API_KEY']}"},
json={"meeting_link": meeting_link, "bot_name": BOT_NAME,
"bot_message": "Hi, I'm Atlas, an AI assistant. Say 'Hey Atlas' to ask.",
"live_audio_required": {"websocket_url": f"{public_wss}/audio"},
"socket_connection_url": {"websocket_url": f"{public_wss}/control"}})
r.raise_for_status()
return r.json()["bot_id"]
def to_48k(pcm16, src_rate):
x = np.frombuffer(pcm16, dtype="<i2").astype(np.float32)
n = int(len(x) * RATE / src_rate)
return np.interp(np.arange(n) * src_rate / RATE, np.arange(len(x)), x).astype("<i2").tobytes()
class Voice:
def __init__(self, ws, bot_id):
self.ws, self.bot_id, self.task = ws, bot_id, None
def speaking(self):
return self.task is not None and not self.task.done()
async def _stream(self, pcm48):
loop, step = asyncio.get_running_loop(), int(RATE * CHUNK_S) * 2
t0 = loop.time()
for n, i in enumerate(range(0, len(pcm48), step)):
wait = t0 + n * CHUNK_S - LEAD_S - loop.time()
if wait > 0:
await asyncio.sleep(wait) # real-time pace after the lead
await self.ws.send(json.dumps({
"command": "sendaudio", "bot_id": self.bot_id,
"audiochunk": base64.b64encode(pcm48[i:i + step]).decode(),
"sample_rate": RATE, "encoding": "pcm16", "channels": 1, "endianness": "little"}))
def say(self, pcm16, src_rate=24000):
self.task = asyncio.create_task(self._stream(to_48k(pcm16, src_rate)))
async def barge_in(self):
self.task.cancel() # stop feeding new chunks
await self.ws.send(json.dumps({"command": "interrupt", "bot_id": self.bot_id,
"action": "clear_audio_queue"}))
VOICES = {}
async def control(ws):
ready = json.loads(await ws.recv()) # {"type": "ready", "bot_id": ...}
VOICES[ready["bot_id"]] = Voice(ws, ready["bot_id"])
await ws.wait_closed()
VOICES.pop(ready["bot_id"], None)
async def audio(ws):
bot_id, heard = json.loads(await ws.recv())["bot_id"], 0.0
async for frame in ws:
# 0x01 (Meet, Teams: 48 kHz) or 0x04 + u32 LE rate + u8 channels (Zoom),
# then u16 LE + speaker_id, u16 LE + name, PCM
if isinstance(frame, str) or frame[0] not in (0x01, 0x04):
continue
rate, o = (48000, 1) if frame[0] == 0x01 else (int.from_bytes(frame[1:5], "little"), 6)
n = int.from_bytes(frame[o:o + 2], "little")
m = int.from_bytes(frame[o + 2 + n:o + 4 + n], "little")
name = frame[o + 4 + n:o + 4 + n + m].decode()
pcm = np.frombuffer(frame[o + 4 + n + m:], dtype="<i2").astype(np.float32)
loud = pcm.size > 0 and np.sqrt(np.mean(pcm ** 2)) > SPEECH_RMS
human = name not in (BOT_NAME, "NoSpeaker")
heard = heard + pcm.size / rate if (human and loud) else 0.0
voice = VOICES.get(bot_id)
if voice and voice.speaking() and heard >= HOLD_S:
await voice.barge_in()
heard = 0.0
async def route(ws):
await (control(ws) if ws.request.path.endswith("/control") else audio(ws))
async def main():
async with serve(route, "0.0.0.0", 8765): # put TLS (wss://) in front
await asyncio.Future()
Audio must be raw PCM16, little-endian and mono, base64-encoded with no WAV header; 48 kHz is recommended, so 24 kHz model output is resampled first. The docs suggest 0.5 to 2 second chunks; the short end keeps the tail short.
How do you stop the agent from hearing itself?
Echo is the agent's own speech coming back as input, so it interrupts itself or answers its own sentence.
- Drop your own name. Give the bot a distinctive
bot_nameand ignore frames and transcript events attributed to it, as the sample does. - Watch for speakerphones. A room system with weak echo cancellation can replay your agent into its microphone under a person's name. While the agent speaks, transcript words that match its own sentence are echo.
- Keep two agents apart. Two agents in one call trigger each other. Give each its own wake phrase and ignore known bot names.
Should you use MIA or your own OpenAI Realtime or Gemini Live pipeline?
Use MIA when you want a speaking agent without running a voice loop: it runs inside MeetStream with wake words and interruption handling, and you attach it with agent_config_id on create_bot. Bring your own pipeline when the agent's brain lives in your stack and you need control of prompts, tools and memory.
| MIA (hosted) | Your bridge plus OpenAI Realtime | Your bridge plus Gemini Live | |
|---|---|---|---|
| What you run | Nothing for the voice loop | A server for the audio and control WebSockets, plus the model session | Same as OpenAI |
| When to speak | Wake words built in | server_vad or semantic_vad, plus your addressee rule | Automatic activity detection, plus your addressee rule |
| Barge-in | Built in | Watch speech_started, send interrupt, truncate | Watch interrupted, send interrupt |
| Audio formats | Handled for you | 48 kHz to 24 kHz in; 24 kHz to 48 kHz out | 48 kHz to 16 kHz in; 24 kHz to 48 kHz out |
| Cost | MIA is priced separately | $0.35 per bot hour plus your model bill | $0.35 per bot hour plus your model bill |
The bridge is mostly format work. MeetStream sends PCM16 mono, at 48 kHz on Google Meet and Teams (Zoom frames carry their rate in the header); OpenAI Realtime defaults to 24 kHz PCM, and Gemini Live takes 16 kHz in and returns 24 kHz. Those are exact halves and thirds of 48 kHz, so resampling is cheap. The meeting audio is mixed, so gate what you forward with your activation rule, or the model will answer every speaker in the room.
For MIA's own speed and control trade-off, see MIA real-time vs pipeline.
How does each platform admit a bot that will speak?
Every platform treats the bot as a participant, so the meeting's own access rules decide whether it gets in. Plan for a waiting room on Google Meet and Teams, and for an authorizing user on Zoom.
| Platform | How a speaking bot gets in | What can stop it | Events to handle |
|---|---|---|---|
| Google Meet | As a guest that asks to join, or a signed-in Workspace user | No host to admit it; access settings that hold outsiders | bot.in_waiting_room, bot.denied, bot.notallowed |
| Zoom | Zoom Meeting SDK, with an OBF or ZAK token fetched from your obf_url or zak_url | Authorizing user absent or leaving; host declines recording | bot.kicked, bot.recording_permission_denied |
| Microsoft Teams | As a guest through the lobby, or a signed-in bot | Nobody to admit it; anonymous join turned off | bot.in_waiting_room, bot.denied, bot.notallowed |
Google Meet. Google's meeting access settings decide who knocks. With Open access nobody does. With Trusted, outsiders join directly only if invited through Google Calendar. With Restricted, only Calendar invitees and people the host invites from the call skip the knock. A guest bot knocks; a signed-in Workspace bot invited to the event joins like any invitee. See the Google Meet bot API.
Zoom. Zoom bots need a one-time Marketplace app setup. Since 2 March 2026, Zoom requires an On Behalf Of (OBF) token when a Meeting SDK app joins a meeting outside the app owner's account; a ZAK covers joining as a Zoom user. Your server runs OAuth and mints the token; MeetStream fetches it from obf_url at join time and does not store end-user refresh tokens. Per Zoom's OBF FAQ, the authorizing user must be present, and the session ends when they leave. For a voice agent, that is usually the person who invited it. See the Zoom bot API.
Microsoft Teams. Per Microsoft's lobby documentation, organizers and presenters admit people, and anonymous participants wait unless everyone may bypass the lobby. If an organization turns off anonymous join, a guest bot cannot join at all; a signed-in bot avoids both. See the Teams bot API.
On every platform, set automatic_leave.waiting_room_timeout so an unadmitted bot gives up, and tell your user to admit it on bot.in_waiting_room. Workspace webhooks are signed and logged, and you can resend any delivery from the dashboard. If state looks stale, poll GET /api/v1/bots/{bot_id}/status.
How should a voice agent tell people it is an AI?
Say it three ways: in its name, its first chat message and its first spoken line. Use a name like "Atlas (AI assistant)", never a person's name.
Set bot_message on create_bot to post an introduction when the bot joins, and use POST /api/v1/bots/{bot_id}/send_image to show a card such as "AI assistant: say Hey Atlas" as its video frame.
In the EU, Article 50(1) of the AI Act, applicable from 2 August 2026, requires AI systems that interact directly with people to be designed so they know they are dealing with an AI, unless that is obvious. If the agent records, recording laws apply too: California Penal Code 632 requires all parties' consent to record a confidential communication, and Washington's RCW 9.73.030 treats a recorded announcement as consent. This is not legal advice.
Give people an off switch. If someone asks the agent to leave, GET /api/v1/bots/{bot_id}/remove_bot removes it.
What should you build first?
Ship in the order that keeps the room's trust:
- Chat first. A wake word agent that answers with
POST /api/v1/bots/{bot_id}/send_messageproves addressee detection without talking over anyone. - Then voice. Add
sendaudio, speaking only after theend_of_turnof an addressed question, with the floor free. - Then barge-in and echo. Test with a speakerphone in the room.
- Then latency. Trace the four hops before tuning any of them.
Or let MIA handle steps two and three, and spend that time on what the agent knows.
Frequently asked questions
Can an AI agent talk in a Zoom or Google Meet call?
Yes. A meeting bot joins as a participant and plays audio through its microphone, so everyone hears the agent. With MeetStream you send speech with the sendaudio command, or use MIA, which listens and speaks for you.
Can the agent speak privately to one participant?
No. There is no documented way to play bot audio to one person, so the whole meeting hears it, and the meeting chat is visible to everyone too. For private help, show it in your own app.
How fast should a meeting voice agent respond?
People leave about 200 ms between turns, which an agent cannot match. Aim to start speaking within about 1.5 seconds of the turn that addressed it, and measure each hop to see where the time goes.
What is barge-in for a voice agent?
Barge-in is a person talking while the agent speaks. The agent should stop sending audio, clear the bot's queue with interrupt, and tell its model how much of the answer was heard.
Do I need OpenAI Realtime or Gemini Live?
No. You can use MIA or chain your own speech-to-text, language model and TTS. Realtime speech models shorten the path to first audio, but you still need rules for who the agent answers in a group.
About the author: Navaneeth (Nav) Jawahar is co-founder and CTO of MeetStream, where he leads the bot infrastructure and MIA voice agent work.
