developers
How to build a meeting bot in 2026 — the three ways, with working code
Every "meeting bot" you have ever seen in a Zoom, Google Meet or Teams call — Otter, Fireflies, Read, Fathom, the one your sales team runs — is the same object underneath: a web browser, running in a container on somebody's cluster, that opened the meeting link and clicked Join. There is no generally available bot API in Zoom, Meet or Teams that hands a program the audio. (Google's Meet Media API will, one day — as of September 2026 it is a Developer Preview in which every participant in the call has to be enrolled, which rules out any meeting with a guest in it.) So a program has to show up as a participant, and the only thing that can be a participant is a browser — or, on Zoom alone, a process built on Zoom's native Meeting SDK, which is the same amount of work in C++ (Recall's write-up of doing exactly that is a good measure of it).
That one fact decides everything about how you build one. There are exactly three ways, and this post gives you working code for each, the published prices, and a decision table at the end.
- Drive the browser yourself — Playwright or Puppeteer, a virtual display, a virtual sound card, FFmpeg. Free, and the most work.
- Rent the browser — a bot API like Recall.ai or MeetingBaaS runs the container and sends you webhooks. Fast, metered by the hour.
- Join a room that has an API — on TeamMeet a bot is one WebSocket and one key, because the room itself is the API. Thirty lines, no audio pipeline, but it only works for meetings held there.
If you came here for "how to make a meeting bot for Zoom" specifically, ways one and two are yours. Way three is the one worth knowing exists before you commit to a browser fleet.
What a meeting bot has to do
Whatever route you take, the job is the same five steps:
- Join the meeting as a participant, and get admitted (waiting rooms, "Ask to join", host approval).
- Capture the meeting's audio, and usually the video.
- Transcribe it, and try to work out who said what.
- Deliver the recording, the transcript and whatever you derive from it (summary, action items) somewhere useful.
- Leave — on a signal, when everyone else has, or on a timeout — and clean up.
Step 3 is where every bot on every platform has the same structural weakness: a participant hears one mixed stream. The bot does not get Jack's microphone and Priya's microphone; it gets the room. Who spoke is then reconstructed from the platform's active-speaker signal and from voice diarisation, which is a guess, and it is wrong exactly when it matters — two people talking at once, a name the platform did not report, a shared conference-room mic. We wrote up why that is structural rather than a model problem in Why a meeting bot needs a whole browser to take notes and The AI put your words in someone else's mouth. Keep it in mind as you read the code: nothing below fixes it, because nothing can from inside a participant's seat.
Way 1 — drive the browser yourself
This is the architecture every notetaker company started with, and it is what the open-source screenappai/meeting-bot (MIT, TypeScript, Playwright) runs in production for Meet, Zoom and Teams. The moving parts:
- A browser under automation. Chromium, launched by Playwright or Puppeteer with fake media devices so the "allow camera?" prompt never blocks, and a persistent profile if the platform demands a signed-in account.
- A virtual display. Xvfb, so the browser has a screen to render the call onto even though nobody is watching.
- A virtual sound card. PulseAudio with a null sink, so the meeting's audio has somewhere to go.
- A recorder. FFmpeg reading the X display and the Pulse sink into an MP4.
- A join script that knows the platform's pre-join page — the name field, the "Ask to join" button, the lobby text — and waits for admission.
- A transcription step afterwards (or a streaming one), plus diarisation.
- A container per meeting, because one Chrome rendering one call is 1–2 CPU cores and a couple of gigabytes of RAM, and it cannot be in two meetings.
Here is the Google Meet join flow, as that project actually implements it (selectors quoted from its GoogleMeetBot.ts at the time of writing — they change when Meet's UI does, which is the first thing that breaks):
// Playwright. Adapted from screenappai/meeting-bot (MIT).
await page.goto(meetingUrl, { waitUntil: 'domcontentloaded' });
// Guest pre-join: a name, no devices.
await page.locator('input[type="text"]').first().fill('Notes bot');
const noDevices = page.getByText(/Continue without microphone and camera/i);
if (await noDevices.isVisible().catch(() => false)) await noDevices.click();
// Whichever button Meet is showing today.
for (const label of ['Ask to join', 'Join now', 'Join anyway']) {
const b = page.getByRole('button', { name: label });
if (await b.isVisible().catch(() => false)) { await b.click(); break; }
}
// Admitted when the in-call chrome appears; parked when the lobby text does.
await page.waitForSelector('button[aria-label^="People"], button[aria-label="Leave call"]', { timeout: joinWaitMs });
// Meet's "Got it" modals get in the way of the recording; dismiss them in a loop.
And the capture, which is not browser code at all:
Xvfb :99 -screen 0 1280x720x24 &
pulseaudio --start --exit-idle-time=-1
pactl load-module module-null-sink sink_name=meet
export DISPLAY=:99 PULSE_SINK=meet
# ...launch the browser here, then:
ffmpeg -f x11grab -video_size 1280x720 -framerate 25 -i :99 \
-f pulse -i meet.monitor \
-c:v libx264 -preset veryfast -c:a aac recording.mp4
What you should know before choosing this route, because the open-source project's own README says it and it matches what we found building the same thing:
- It only works for links a guest can open. Meetings that require sign-in, an enterprise SSO account, or an authenticated waiting room are out unless you maintain real accounts for the bot, which is its own problem.
- Admission is a human's decision. "Ask to join" means a person has to say yes, every time, and Zoom's host gets a dialog. Build a lobby timeout, or your fleet fills with bots waiting in waiting rooms.
- The selectors rot. Meet, Zoom and Teams change their pre-join pages without notice. The project the code above is adapted from carries three English join-button labels and their German equivalents for a reason.
- It costs a container-hour per meeting-hour. A 2-vCPU / 4 GB container is roughly $0.05–$0.10 an hour on a cloud, before transcription. That is cheaper than renting — until you count the on-call engineer.
- Bot detection is real and getting worse. Platforms increasingly flag automated Chrome; the projects that survive rotate profiles and avoid
navigator.webdrivertells. Read Puppeteer Google Meet bot: what works and what breaks before you start.
Pick this when the data cannot leave your perimeter, when you need something the APIs do not expose, or when you are a platform yourself and the fleet is the product.
Way 2 — rent the browser: Recall.ai, MeetingBaaS
A bot API runs the container fleet above and gives you a REST call. You send a meeting URL; a participant appears; webhooks tell you what happened; the recording and transcript come back as download links. This is how TeamMeet's own notetaker works — it is a Recall.ai bot underneath, and we say so on the page — so the code below is the code we run.
Creating a Recall bot is one POST:
// Recall.ai — note the `Token` scheme, not Bearer.
const r = await fetch('https://us-east-1.recall.ai/api/v1/bot/', {
method: 'POST',
headers: { authorization: 'Token ' + process.env.RECALL_API_KEY, 'content-type': 'application/json' },
body: JSON.stringify({
meeting_url: 'https://zoom.us/j/123456789?pwd=…',
bot_name: 'Notes bot',
recording_config: {
video_mixed_mp4: {}, // keep the mixed video
participant_events: {}, // who joined, left, spoke
// transcript: { provider: { meeting_captions: {} } } // or ask for it after the call
},
automatic_leave: { waiting_room_timeout: 1200, noone_joined_timeout: 1200,
everyone_left_timeout: { timeout: 60, activate_after: 60 } },
metadata: { owner: 'user_123' },
}),
});
const bot = await r.json(); // { id, status_changes, ... }
Then a webhook endpoint receives bot.joining_call, bot.in_call_recording, bot.call_ended, bot.done, and the artifact events (recording.done, transcript.done, video_mixed.done). The transcript download is an array of turns — { participant: { id, name }, words: [{ text, start_timestamp, end_timestamp }] } — and a participant the platform did not name arrives with name: null, which is the mixed-stream problem showing up as a field.
Published prices, as of September 2026:
| Recall.ai | MeetingBaaS | |
|---|---|---|
| Recording | $0.50 per hour, prorated to the second (pricing) | 1 token per hour; tokens are $0.35–$0.50 each depending on the pack (pricing) |
| Transcription | +$0.15/h for the built-in option | +0.25 tokens/h with Gladia, +0.05 tokens/h with your own key |
| Free tier | First 5 hours free; 7 days of free storage per recording | 8 free hours on Pay as You Go |
| Plans | Pay As You Go, Launch (custom), Enterprise (custom); startups get $0.25/h for their first 10,000 hours | Pay as You Go (free), Pro $99/mo, Scale $199/mo, Enterprise $299/mo, plus tokens |
| Storage | $0.05 per recorded hour kept 30 days after the free week | — |
| Platforms | Zoom, Google Meet, Microsoft Teams, Webex, Slack Huddles | Zoom, Google Meet, Microsoft Teams |
So a bot-hour with a transcript is roughly $0.65 on Recall and $0.44–$0.63 on MeetingBaaS at list, before the monthly plan. We compared the two in more detail in Recall.ai vs MeetingBaaS.
Pick this when you need Zoom, Meet and Teams today and your users are not going to change where they meet. It is the right default for most products, and it is why our own notetaker is built on it rather than on a fleet we run.
Way 3 — join a room that has an API
Ways one and two exist because the incumbents' rooms have no door for programs. If the room does, the whole stack collapses into a socket. This is what the TeamMeet agent API is: a program joins with a key, is listed to everyone in the room as an agent (no hidden listeners), and receives every transcript line with the speaker already attached, because each participant's browser transcribes its own microphone and the room never hears a mixed stream.
The complete bot, verified against the live server before this was published. Node 22 or newer, no packages:
// A meeting bot in one file. Node 22+ (global fetch and WebSocket), no packages.
const HOST = process.env.TEAMMEET_HOST || 'teammeet.ai';
const http = 'https://' + HOST, wss = 'wss://' + HOST;
// 1. A key. Anyone can mint one, right now. It is shown once — save it.
const { key } = await (await fetch(`${http}/api/keys`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Minutes bot', emoji: '📝' }),
})).json();
// 2. A meeting. Started with the key, so the key is let straight in;
// in anybody else's meeting the bot knocks and a person admits it.
const { code } = await (await fetch(`${http}/api/meeting`, {
method: 'POST', headers: { authorization: `Bearer ${key}` },
})).json();
console.log('meeting:', `${http}/${code}`);
// 3. Join. One socket. No browser, no container, no audio pipeline.
const ws = new WebSocket(`${wss}/`);
ws.onopen = () => ws.send(JSON.stringify({ type: 'agent-hello', key, meeting: code }));
ws.onmessage = ({ data }) => {
const m = JSON.parse(data);
if (m.type === 'agent-ready') console.log('in the room as', m.you.name, '· may:', m.can.join(', '));
if (m.type === 'agent-refused') console.error('refused:', m.why);
// 4. Every line arrives with the speaker attached: each microphone is its own stream.
if (m.type === 'transcript') {
const { speaker, text } = m.line;
console.log(`${speaker}: ${text}`);
if (/\b(I'll|I will|by (monday|tuesday|wednesday|thursday|friday))\b/i.test(text)) {
ws.send(JSON.stringify({ type: 'note', text: `${speaker}: ${text}` })); // pinned in the room's chat
}
}
};
Run it, open the meeting link it prints, say "I will have the pricing page done by Friday", and the bot's note appears in the room's chat under its own name. What it printed when we ran it against teammeet.ai on 17 September 2026, with one person in the room:
meeting: https://teammeet.ai/xuz-anee-eta
in the room as Minutes bot · may: say, chat, draw, note
Jack: I will have the pricing page done by Friday.
and in the room's chat: 📝 Minutes bot noted: Jack: I will have the pricing page done by Friday.
An agent may say (spoken aloud in the room), chat, draw (a picture from a prompt, shown to everyone), note and leave, each rate-limited so a bot cannot talk over people. Every action is attributed to the agent by name; the host can remove it like anyone else, or turn agents off for the meeting. The same room is reachable over MCP if the "bot" is Claude or another assistant rather than code you wrote. The API is free on every plan.
The limit is obvious and worth saying plainly: this only works for meetings held on TeamMeet. If your users' meetings are on Zoom, you need way one or two — which is why we offer a Recall-backed notetaker for those, with the honest line printed on every transcript it brings back: one mixed stream, N lines nobody could be named for.
The comparison
| Drive the browser | Rent the browser | Room with an API | |
|---|---|---|---|
| Works on Zoom / Meet / Teams | Yes (guest links) | Yes | No — TeamMeet rooms only |
| Time to first transcript line | Days to weeks of work | An afternoon | Minutes (the 30 lines above) |
| Cost per meeting-hour | Container time, ~$0.05–0.10, plus transcription, plus an engineer | ~$0.44–0.65 at list | $0 |
| Who spoke | Guessed from a mixed stream | Guessed from a mixed stream, plus the platform's participant list | Known — each microphone is its own stream |
| Admission | Human clicks, every time | Human clicks, every time | The key is admitted to meetings it started; knocks elsewhere |
| Breaks when | The platform changes its UI | Rarely — the vendor maintains the fleet | Never for this reason; there is no UI to drive |
| Hidden from participants? | Only if you make it so (don't) | Appears as a participant | Listed as an agent, by name, always |
Which one to build
- Shipping a notetaker for Zoom/Meet/Teams users? Rent the browser. Budget about $0.65 an hour, build your product on the transcript, and be honest with users about speaker attribution — it is the thing they will complain about first, and it is not your model's fault.
- **Regulated, air-gapped, or the bot fleet is your product?** Drive the browser. Start from screenappai/meeting-bot rather than from zero, and expect to babysit selectors.
- **Building something that lives in the meeting** — a facilitator, a translator, a coach, an assistant people can ask out loud — and you can choose where the meeting happens? Use a room with an API. It is the only route where the program is a first-class participant rather than a camera pointed at the window.
If the third one is you, mint a key at teammeet.ai/developers and paste the file above. If it is not, our notetaker page is here, and both bot vendors' quickstarts are linked in the sources.
Sources
- Recall.ai, How to build a meeting bot — https://www.recall.ai/blog/how-to-build-a-meeting-bot
- Recall.ai, How to build a Zoom bot from scratch — https://www.recall.ai/blog/how-to-build-a-zoom-bot
- Recall.ai pricing — https://www.recall.ai/pricing
- MeetingBaaS pricing — https://www.meetingbaas.com/en/pricing
- screenappai/meeting-bot (MIT) — https://github.com/screenappai/meeting-bot
- Google Meet Media API overview (Developer Preview) — https://developers.google.com/meet/media-api/guides/overview
- TeamMeet agent API — https://teammeet.ai/developers