> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.meetstream.ai/build-with-ai/agent-quickstarts/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server. # Agent Quickstarts > Paste the MeetStream Agent Guide into your coding agent, then run a task prompt: post-call notetaker, real-time transcript dashboard, Google Calendar auto-join, MIA voice agent, Recall.ai migration, or MCP bot debugging. Each prompt ships with acceptance criteria. This page is for building on MeetStream with a coding agent. It has three parts: 1. **Point your agent here first.** The machine-readable surfaces and tools, and when to use each. 2. **The MeetStream Agent Guide.** One canonical block you paste once. It holds the setup split, hard rules, retry helper, webhook model, and transcription flows. 3. **Quickstarts.** Short task prompts that build on the guide, each with acceptance criteria your agent must meet before it says "done". ## Point your agent here first | Resource | Where | Use it for | | --------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | llms.txt | [docs.meetstream.ai/llms.txt](https://docs.meetstream.ai/llms.txt) | Page index. The first thing an agent should fetch | | Markdown twin | Append `.md` to any docs URL | Clean page content with no HTML chrome, e.g. `docs.meetstream.ai/guides/webhooks/webhooks-and-events.md` | | OpenAPI spec | [docs.meetstream.ai/openapi.json](https://docs.meetstream.ai/openapi.json) | Exact paths, fields, and types for codegen and validation | | Docs-search MCP | `https://docs.meetstream.ai/_mcp/server` | Read-only search over these docs. No key needed | | Action MCP | [MeetStream MCP Server](/build-with-ai/meetstream-mcp-server) | Acts on your account: create bots, read status and details, fetch transcripts, debug a real session | | Agent skills | [Agent Skills](/build-with-ai/agent-skills) | Instructions loaded on demand that encode the API surface and its gotchas | | SDKs | [Official SDKs](/build-with-ai/sdks) | Typed TypeScript and Python clients that already handle 202, 507, and retries | | CLI | [MeetStream CLI](/build-with-ai/meetstream-cli) | Shell access with `--json` output, plus `meetstream listen` to print webhooks during development | **Which to use when.** Always give a coding agent the guide below, plus the docs-search MCP or the `.md` twins so it reads the docs instead of guessing. Add the action MCP when you want the agent to create test bots and inspect real sessions while it builds or debugs. That is the fastest way to answer "why did this bot not join?". Install the skills in Claude Code or Cursor so the same rules load automatically in every session. For generated application code, prefer the SDKs over raw `fetch`: the guide's helper is for projects that can't take a dependency. Use the CLI for one-off checks and for watching webhooks locally. #### Claude Code ```bash # Skills (inside Claude Code) /plugin marketplace add meetstream-ai/claude-plugin /plugin install meetstream@meetstream-ai # Action MCP (REST uses Token, the MCP server uses Bearer) claude mcp add --transport http meetstream https://mcp.meetstream.ai/mcp \ --header "Authorization: Bearer YOUR_API_KEY" # Docs-search MCP (no key) claude mcp add --transport http meetstream-docs https://docs.meetstream.ai/_mcp/server ``` Save the Agent Guide below as `MEETSTREAM.md` in your repo root and add the line `@MEETSTREAM.md` to your `CLAUDE.md`. Claude Code then loads it in every session. #### Cursor Install the [MeetStream Cursor plugin](https://github.com/meetstream-ai/meetstream-cursor-plugin), which bundles the action MCP and the skills. Then set your API key under **Plugins → Configure**. Save the Agent Guide below as `.cursor/rules/meetstream.mdc`, starting with this frontmatter so it applies to every chat: ```markdown --- description: MeetStream meeting bot API rules alwaysApply: true --- ``` #### Codex and others Codex reads `AGENTS.md` from the repo root, so paste the Agent Guide there, or into a file that `AGENTS.md` links to. For any other agent, paste the guide at the top of the conversation and then paste a quickstart prompt. If your client speaks MCP, point it at `https://mcp.meetstream.ai/mcp` with `Authorization: Bearer YOUR_API_KEY`, or run `npx -y @meetstream/mcp` locally with `MEETSTREAM_API_KEY` set. See [MeetStream MCP Server](/build-with-ai/meetstream-mcp-server). ## The MeetStream Agent Guide Copy this whole block into your agent. Every quickstart below assumes the agent has it. The guide keeps the human's steps separate from the agent's, and it ends with a verification checklist the agent must run. > **Warning** > > Never paste a real API key into the guide, a prompt, or a committed file. The guide tells the agent to read `MEETSTREAM_API_KEY` from the environment. ````markdown # MeetStream Agent Guide You are building on MeetStream, an API that sends bots into Zoom, Google Meet, and Microsoft Teams meetings to record, transcribe, stream, and speak. Follow this guide exactly. When it conflicts with your prior knowledge of meeting-bot APIs, the guide wins. When you need a detail that is not here, fetch the docs as Markdown (section 9) or use the docs-search MCP. Do not guess endpoints or field names. ## 1. Glossary and environment - human: the person you are working with. Only they can do the steps in section 2. - agent: you. - bot: one MeetStream participant in one meeting, identified by `bot_id`. - MEETSTREAM_API_KEY: REST API key. Read it from the environment. Never log, print, commit, or hardcode it. - PUBLIC_BASE_URL: public HTTPS origin that reaches this app, with no trailing slash. In development it is the tunnel URL (ngrok or cloudflared). MeetStream cannot reach localhost. - WEBHOOK_PATH_TOKEN: a long random string you generate and put in the webhook path. Per-bot `callback_url` deliveries are not signed, so the unguessable path is what keeps strangers from posting fake events. - MEETSTREAM_WEBHOOK_SECRET (optional): signing secret of a workspace webhook endpoint created in the dashboard. Only workspace endpoints are signed. - callback_url: per-bot URL for lifecycle and post-call webhooks. - live_transcription_required.webhook_url: per-bot URL for live transcript chunks. - transcript_id: the key for post-call transcripts. It is not the bot_id, and it is never in a webhook. At startup, fail fast with a clear message if MEETSTREAM_API_KEY or PUBLIC_BASE_URL is missing, or if PUBLIC_BASE_URL does not start with https://. ## 2. Human-required setup (stop and ask; do not work around these) The human does these. You check the result. 1. API key: create one at https://app.meetstream.ai/api-key and export it as MEETSTREAM_API_KEY. You check: `GET /bots` returns 200 (401 = header missing, 403 = key rejected). 2. Transcription provider keys: for deepgram, assemblyai, sarvam, or jigsawstack (including their streaming variants), add that provider's key in the dashboard at https://app.meetstream.ai/integrations. `meetstream_streaming` and `meeting_captions` need no external key. You check: a create_bot 400 that names the provider means its key is missing. Report it to the human; do not switch providers silently. 3. Public HTTPS URL: webhooks require HTTPS. The human runs `ngrok http ` or `cloudflared tunnel --url http://localhost:` and gives you the URL as PUBLIC_BASE_URL. A reserved or static domain avoids re-creating bots after every tunnel restart. You check: a health route you add (for example `curl -X POST $PUBLIC_BASE_URL/healthz`) answers from outside the machine. 4. Zoom only: a Zoom Marketplace General App with Meeting SDK enabled, with its Client ID and Secret saved in the dashboard Integrations page. Guide: https://docs.meetstream.ai/guides/app-integrations/zoom-marketplace-app-setup.md Google Meet and Teams need no setup. 5. A live test meeting: the human opens a real meeting, gives you the link, and admits the bot from the lobby when it knocks. The agent does everything else: code, payloads, webhook server, storage, UI, tests. ## 3. Hard rules - Base URL: `https://api.meetstream.ai/api/v1`. - REST auth header: `Authorization: Token `. The word is Token, NOT Bearer. The MCP server at mcp.meetstream.ai is the exception: it uses `Bearer`. - Create: `POST /bots/create_bot`. Required: `meeting_link` (NOT `meeting_url`) and `bot_name`. Success is 201 with `bot_id`. - Send an `Idempotency-Key` header on every create. Generate it ONCE, outside the retry loop, and reuse it on every attempt. A replay returns 507 with the original bot: 507 is success, never retry it and never treat it as an error. - 202 means "accepted, not ready yet". It is in the 2xx range but it is not data. Poll with a fixed cap (interval and max attempts). Never loop forever. - Retry 429 (honor Retry-After), 500, and 503 with exponential backoff. Never retry 400, 401, 403, or 404: fix the request. 400 is never transient; log its `message`. - `video_required` defaults to true. Set `"video_required": false` for transcript-only bots. - `automatic_leave.in_call_recording_timeout` has a 600 second floor. Below 600 is a 400. Only set automatic_leave fields you need, since out-of-range values are 400s. - One transcription provider per request under `recording_config.transcript.provider`. - MIA voice agents: pass `agent_config_id` and nothing else agent-related. Do NOT send `socket_connection_url` or `live_audio_required` with it. Those are for bring-your-own bridges, and adding them is the usual cause of an agent that joins but never speaks. - `GET /bots/{bot_id}/remove_bot` makes a bot leave (it is a GET). Data is kept. `DELETE /bots/{bot_id}/delete` destroys data and is irreversible: never call it without the human's explicit confirmation. - Media and transcript download URLs are presigned and expire (about 1 hour). Store `bot_id` and `transcript_id`, never the URLs. Re-fetch a URL when you need it. - Webhooks are delivered once and NOT retried. Return 2xx immediately, then process asynchronously. A slow or failing handler loses events. - Echo your own IDs through `custom_attributes` on create. They are echoed back on lifecycle webhooks and live transcript chunks (not on `data_deletion`), which is how you route events to your own records. ## 4. API client with retries (TypeScript) Use the official SDK (`@meetstream/sdk`, `meetstream-sdk` for Python) when the project allows dependencies. Otherwise use this helper for every call. ```ts const BASE_URL = 'https://api.meetstream.ai/api/v1'; const RETRYABLE = new Set([429, 500, 503]); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const backoff = (n: number) => Math.min(30_000, 1_000 * 2 ** n) + Math.random() * 250; export class MeetStreamError extends Error { constructor(public status: number, public body: string) { super(`MeetStream ${status}: ${body}`); } } // Returns { status, data }. 201/200 = done, 202 = not ready (caller polls with a cap), // 507 = idempotent replay of an earlier create (success, same bot). export async function meetstream( path: string, init: RequestInit & { headers?: Record } = {}, maxRetries = 4, ): Promise<{ status: number; data: T }> { for (let attempt = 0; ; attempt++) { let res: Response; try { res = await fetch(`${BASE_URL}${path}`, { ...init, headers: { Authorization: `Token ${process.env.MEETSTREAM_API_KEY}`, 'Content-Type': 'application/json', ...init.headers, }, }); } catch (err) { // Network failure. Safe to retry a create only because it carries an Idempotency-Key. if (attempt >= maxRetries) throw err; await sleep(backoff(attempt)); continue; } const text = await res.text(); let data: any = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; } if (res.ok || res.status === 507) return { status: res.status, data }; if (!RETRYABLE.has(res.status) || attempt >= maxRetries) { throw new MeetStreamError(res.status, text); // 400/401/403/404: fix, don't retry } const retryAfter = Number(res.headers.get('retry-after')); await sleep(res.status === 429 && retryAfter > 0 ? retryAfter * 1000 : backoff(attempt)); } } ``` Creating a bot: ```ts import { randomUUID } from 'node:crypto'; const idempotencyKey = randomUUID(); // once, outside any retry loop const { status, data } = await meetstream('/bots/create_bot', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(payload), }); // status is 201 (created) or 507 (replayed the same bot). Both are success. const botId: string = data.bot_id; ``` ## 5. Create-bot payloads Post-call transcript (notetaker): ```json { "meeting_link": "https://meet.google.com/abc-defg-hij", "bot_name": "Notetaker", "video_required": false, "callback_url": "https://PUBLIC_BASE_URL/webhooks/meetstream/WEBHOOK_PATH_TOKEN", "custom_attributes": { "meeting_id": "your-internal-id" }, "recording_config": { "transcript": { "provider": { "deepgram": { "model": "nova-3", "language": "en" } } } } } ``` Other post-call providers: `assemblyai`, `sarvam`, `jigsawstack`, `meetstream`. Read the provider page before changing its config block. Real-time transcript (streaming): ```json { "meeting_link": "https://meet.google.com/abc-defg-hij", "bot_name": "Live Captions", "video_required": false, "callback_url": "https://PUBLIC_BASE_URL/webhooks/meetstream/WEBHOOK_PATH_TOKEN", "live_transcription_required": { "webhook_url": "https://PUBLIC_BASE_URL/webhooks/meetstream-live/WEBHOOK_PATH_TOKEN" }, "recording_config": { "transcript": { "provider": { "meetstream_streaming": {} } } } } ``` Other streaming providers: `deepgram_streaming`, `assemblyai_streaming`, `jigsawstack_streaming`, `meeting_captions`. Setting `live_transcription_required` without a streaming provider is a 400. Optional on any bot: `join_at` (ISO 8601) to schedule ahead, `recording_config.retention` as `{ "type": "timed", "hours": N }` (default 720 hours). ## 6. Webhooks The verified model (checked against 4,139 production deliveries). Trust it over older examples. Envelope - Every delivery has `event`. Most also have `bot_event`, which equals `event` on every event EXCEPT terminals. Some deliveries have no `bot_event` (manifest.completed, manifest.skipped, bot.transcriptionready, bot.uploading, participant_events.*), so read `bot_event ?? event` for the specific name. - Every delivery has an ISO 8601 `timestamp`. - Dedupe on `{bot_id, bot_event ?? event, timestamp}`. - `participant_events.*` are nested: the bot id is at `data.bot.id`, and there is no top-level bot_id, bot_status, or bot_event. - `transcript_id` is never in a webhook. `data_deletion` has no custom_attributes. Terminals (two layers) - Every ending arrives exactly once with `event: "bot.stopped"`. `bot_event` is the reason: | bot_event | status_code | bot_status | meaning | |----------------|-------------|-------------------------|--------------------------------------| | bot.stopped | 200 | Stopped | clean exit (meeting ended, removed, timeout) | | bot.kicked | 200 | Stopped | a participant removed the bot | | bot.notallowed | 500 | NotAllowed | never admitted (lobby timeout) | | bot.denied | 500 | Denied | host refused entry or recording | | bot.failed | usually 500 | FAILED / ERROR / Failed | crashed | - No delivery ever has `event: "bot.kicked"` (or notallowed, denied, failed). Those names only appear in `bot_event`. - `status_code` is not always 200 on bot.stopped. Branch on `bot_event`, not `bot_status` (a kick and a clean exit both say "Stopped", and failure casing varies). If `bot_event` is missing, map `bot_status` case-insensitively: notallowed -> bot.notallowed, denied -> bot.denied, error or failed -> bot.failed, else bot.stopped. - `bot.error` is NOT terminal (a streaming provider hiccup); the bot keeps running. Typical order bot.scheduled (if join_at) -> bot.joining -> bot.in_waiting_room -> bot.inmeeting -> bot.recording -> participant_events.* -> bot.leaving -> bot.stopped -> audio.processed / manifest.completed (either order) -> [post-call providers only: transcription.processed or transcription.failed, then bot.transcriptionready] -> video.processed (if video) -> bot.done -> data_deletion (after delete or retention expiry). Never admitted: bot.joining -> bot.in_waiting_room -> bot.leaving -> bot.stopped (bot_event bot.notallowed) -> bot.done. Also possible: audio.skipped, manifest.skipped, transcription.skipped, bot.uploading. bot.done is the final event on EVERY path, including streaming-only bots and bots that never got in. Use it as the single "session finished" signal. Never treat audio.processed as final. Handler (Express): ```ts import express from 'express'; const app = express(); type StopReason = 'bot.stopped' | 'bot.kicked' | 'bot.notallowed' | 'bot.denied' | 'bot.failed'; export function stopReason(p: any): StopReason { if (p.bot_event) return p.bot_event; const s = String(p.bot_status ?? '').toLowerCase(); if (s === 'notallowed') return 'bot.notallowed'; if (s === 'denied') return 'bot.denied'; if (s === 'error' || s === 'failed') return 'bot.failed'; return 'bot.stopped'; } const seen = new Set(); // use your database or Redis in production app.post('/webhooks/meetstream/:token', express.raw({ type: '*/*' }), (req, res) => { if (req.params.token !== process.env.WEBHOOK_PATH_TOKEN) return res.sendStatus(404); // Workspace endpoints only: verify X-MeetStream-Signature over the raw bytes here. res.sendStatus(200); // ack first: deliveries are not retried let p: any; try { p = JSON.parse(req.body.toString('utf8')); } catch { return; } const botId: string | undefined = p.bot_id ?? p.data?.bot?.id; const key = `${botId}|${p.bot_event ?? p.event}|${p.timestamp}`; if (!botId || seen.has(key)) return; seen.add(key); setImmediate(() => handle(p, botId).catch((e) => console.error('webhook', e))); }); async function handle(p: any, botId: string) { switch (p.event) { case 'bot.stopped': return onEnded(botId, stopReason(p), p.message); case 'transcription.processed': return onTranscriptReady(botId); case 'transcription.failed': return onTranscriptFailed(botId, p.message); case 'bot.done': return onSessionFinished(botId); // always last default: return recordEvent(botId, p.bot_event ?? p.event, p.timestamp); } } ``` ## 7. Transcription Post-call flow (deepgram, assemblyai, sarvam, jigsawstack, meetstream): 1. Create the bot with `recording_config.transcript.provider` set to a post-call provider. 2. Wait for the `transcription.processed` webhook. (`transcription.failed` means the run failed: surface `message`, then offer a re-run with `POST /bots/{bot_id}/transcribe`.) 3. `GET /bots/{bot_id}/detail` and read `bot_details.transcript_id`. Fallback: `GET /bots/{bot_id}/transcriptions` lists every run with its `transcript_id` and status. 4. `GET /transcript/{transcript_id}/get_transcript` returns an array of segments: `{ speaker, transcript, start_time, end_time, words[] }`. The text field is `transcript`, NOT `text`. Add `?raw=true` for the provider's raw output. ```ts export async function fetchTranscript(botId: string, attempts = 20, intervalMs = 15_000) { const { data: detail } = await meetstream(`/bots/${botId}/detail`); let transcriptId: string | undefined = detail?.bot_details?.transcript_id; if (!transcriptId) { const { data } = await meetstream(`/bots/${botId}/transcriptions`); transcriptId = data?.transcriptions?.find((t: any) => t.status === 'Success')?.transcript_id; } if (!transcriptId) throw new Error(`No post-call transcript for ${botId}`); for (let i = 0; i < attempts; i++) { const { status, data } = await meetstream(`/transcript/${transcriptId}/get_transcript`); if (status === 200) return data as { speaker: string; transcript: string; start_time: number; end_time: number }[]; await sleep(intervalMs); // 202: still processing } throw new Error(`Transcript ${transcriptId} still not ready after ${attempts} polls`); } ``` Four ways to get a finished transcript, pick by where you are: | From | Call | Use when | |------|------|----------| | The MCP server | `get_transcript` with `bot_id` (optional `wait`) | Prototyping or debugging in chat. It resolves transcript_id for you | | A bot id (REST) | `/bots/{bot_id}/detail` -> `bot_details.transcript_id` -> `/transcript/{id}/get_transcript` | The default in app code (fetchTranscript above) | | All runs of a bot | `/bots/{bot_id}/transcriptions` | After a re-transcribe, to pick or compare runs | | Native captions | `bot_details.caption_file` on `/detail` | The bot used `meeting_captions` | Render segments for people (and for LLM prompts) with one helper, not ad hoc joins: ```ts type Segment = { speaker?: string; transcript?: string; start_time?: number }; const mmss = (s = 0) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`; // Merges consecutive segments from the same speaker into one turn. export function toReadable(segments: Segment[]): string { const turns: { speaker: string; start: number; text: string[] }[] = []; for (const s of segments) { const speaker = s.speaker || 'Unknown speaker'; const text = (s.transcript ?? '').trim(); if (!text) continue; const last = turns[turns.length - 1]; if (last && last.speaker === speaker) last.text.push(text); else turns.push({ speaker, start: s.start_time ?? 0, text: [text] }); } return turns.map((t) => `[${mmss(t.start)}] ${t.speaker}: ${t.text.join(' ')}`).join('\n'); } ``` Real-time flow (meetstream_streaming, deepgram_streaming, assemblyai_streaming, jigsawstack_streaming, meeting_captions): 1. Set a streaming provider AND `live_transcription_required: { webhook_url }`. 2. Chunks POST to that URL during the meeting. Fields: `bot_id`, `speakerName`, `timestamp`, `transcript` (current buffer, may be partial), `words[]` (word, start, end, confidence, speaker, punctuated_word), `is_final` (false = interim, will be replaced; true = committed), `end_of_turn`, `transcription_mode` (observed "raw" for meetstream_streaming), `custom_attributes`. Some providers also send `word_is_final` per word; treat it as optional and decide on the top-level `is_final`. 3. Ack 2xx immediately, same as lifecycle webhooks. ```ts type LiveChunk = { bot_id: string; speakerName?: string; transcript: string; // the speaker's current buffer; interim chunks get replaced is_final: boolean; // true = committed text end_of_turn?: boolean; timestamp: string; words?: { word: string; start: number; end: number; speaker?: string; punctuated_word?: string }[]; custom_attributes?: Record; }; // One interim line per speaker, replaced until it is committed. const interim = new Map(); // key: bot_id|speaker export function onLiveChunk(c: LiveChunk, commit: (botId: string, speaker: string, text: string) => void) { const speaker = c.speakerName ?? 'Unknown speaker'; const key = `${c.bot_id}|${speaker}`; if (c.is_final) { interim.delete(key); if (c.transcript.trim()) commit(c.bot_id, speaker, c.transcript.trim()); } else { interim.set(key, c.transcript); // render as a grey, replaceable line } } ``` 4. Streaming-only bots produce NO post-call transcript: no transcription.processed, and get_transcript returns 202 forever. They still get bot.done. If you need a post-call transcript too, call `POST /bots/{bot_id}/transcribe` after bot.done with a post-call provider in the body; the response carries the new `transcript_id`. ## 8. Verify before you say done Run every check that applies, and report each result with evidence (status codes, event names, IDs). If a check needs the human (admitting the bot, speaking), ask them. - [ ] No API key in source, logs, or git history. Search the repo for the key's value; only placeholders such as YOUR_API_KEY may appear. - [ ] Every REST call sends `Authorization: Token`, and `GET /bots` returns 200. - [ ] Every create sends `meeting_link` and `bot_name` and an Idempotency-Key made once. Replaying the same key returns 507 and your code treats it as success. - [ ] A 400 is surfaced with its `message` and not retried. - [ ] PUBLIC_BASE_URL is HTTPS and reachable from outside, and the webhook path without the right token returns 404. - [ ] The webhook route returns 2xx before any slow work, and the same payload sent twice is processed once. - [ ] A real bot ran end to end, and you logged: bot.joining -> bot.inmeeting -> bot.stopped (with its bot_event) -> bot.done. - [ ] A kick (remove the bot from the meeting UI) is recorded as bot.kicked, and a lobby timeout as bot.notallowed. Neither is recorded as a clean stop. - [ ] Post-call: the transcript was fetched via transcript_id, segments render from `transcript`, and polling stops after its cap. - [ ] Real-time: interim chunks are replaced, not appended, and `is_final` chunks commit. - [ ] Nothing stores a presigned URL. - [ ] No test bot was left running (`GET /bots/{bot_id}/status`), and no data was deleted without the human's confirmation. ## 9. Docs to fetch as Markdown - Index: https://docs.meetstream.ai/llms.txt - OpenAPI: https://docs.meetstream.ai/openapi.json - Webhooks: https://docs.meetstream.ai/guides/webhooks/webhooks-and-events.md - Local webhooks: https://docs.meetstream.ai/guides/webhooks/local-webhook-server.md - Signatures: https://docs.meetstream.ai/guides/webhooks/webhook-signature-verification.md - Post-call: https://docs.meetstream.ai/guides/transcription-recordings/post-call-transcription.md - Live: https://docs.meetstream.ai/guides/transcription-recordings/live-transcription.md - Providers: https://docs.meetstream.ai/guides/transcription-recordings/providers/transcription-providers.md - Errors: https://docs.meetstream.ai/errors.md - Debugging: https://docs.meetstream.ai/guides/help/debugging-bots.md ```` ## Quickstarts Each quickstart is a short prompt that relies on the Agent Guide above. Paste the guide first (or load it through `CLAUDE.md`, a Cursor rule, or `AGENTS.md`), then paste one of these. The acceptance criteria are part of the prompt, so the agent knows what "done" means before it starts. | # | Build | Transcription | Human setup beyond the guide | | - | -------------------------------- | --------------------- | ------------------------------------- | | 1 | Post-call notetaker with summary | Post-call | None | | 2 | Real-time transcript dashboard | Streaming | None | | 3 | Google Calendar auto-join | Post-call | Google OAuth client and refresh token | | 4 | MIA voice agent in a meeting | Handled by the agent | Model and voice provider keys | | 5 | Migrate from Recall.ai | Mapped from your code | Your Recall.ai codebase | | 6 | Debug a bot with the MCP | None | Action MCP connected | #### 1. Post-call notetaker with summary **Goal:** paste a meeting link into a small web app. A bot joins and records audio only. After the meeting, the app shows a speaker-labeled transcript and MeetStream's AI summary. ```markdown Follow the MeetStream Agent Guide above. Build a post-call notetaker (Node + TypeScript, Express, SQLite). Spec: - A page with a form: meeting link. On submit, create a bot with video_required false, a post-call provider (deepgram nova-3 unless I say otherwise), callback_url on PUBLIC_BASE_URL with WEBHOOK_PATH_TOKEN, and custom_attributes.meeting_id set to our own row id. - Store bot_id and every webhook event (name, bot_event, timestamp) per meeting. - On transcription.processed: fetch the transcript via detail -> transcript_id -> get_transcript and store the segments. - On transcription.failed: show the message and a "Re-run transcription" button that calls POST /bots/{bot_id}/transcribe. - On bot.done: mark the meeting finished, then call GET /bots/{bot_id}/summary. Log the raw response the first time and render what it returns. Do not invent field names. - A meeting page showing live status, the end reason (from bot_event), the transcript grouped by speaker, and the summary. Acceptance criteria: - I paste a real Meet link, admit the bot, talk for 2 minutes, and end the call. Without a refresh loop on my side, the page shows the transcript and summary. - If I refuse the bot at the lobby, the page says "denied" (or "not admitted" on timeout), not "completed". - Re-submitting the form after a network error does not create a second bot. - Every item in the guide's "Verify before you say done" list passes, and you report the evidence. ``` **Docs:** [Post-call transcription](/guides/transcription-recordings/post-call-transcription) · [Transcription providers](/guides/transcription-recordings/providers/transcription-providers) · [Webhooks and events](/guides/webhooks/webhooks-and-events) · [Deduplication and idempotency](/guides/features/deduplication-idempotency-keys) #### 2. Real-time transcript dashboard **Goal:** a browser view of what people are saying, updating as they speak, with no external transcription key. ```markdown Follow the MeetStream Agent Guide above. Build a real-time transcript dashboard (Node + TypeScript, Express, Server-Sent Events to the browser, no frontend framework). Spec: - Form: meeting link. Create a bot with video_required false, provider meetstream_streaming, live_transcription_required.webhook_url on /webhooks/meetstream-live/WEBHOOK_PATH_TOKEN, and callback_url on /webhooks/meetstream/WEBHOOK_PATH_TOKEN for lifecycle events. - Live route: ack 2xx immediately, then push each chunk to the browser over SSE, keyed by bot_id. - Browser: one line per speaker turn, labelled with speakerName. While is_final is false, replace the current interim line. When is_final is true, commit it and start a new line. - Show a status pill driven by lifecycle webhooks (joining, in waiting room, in meeting, ended with reason). On bot.done show "Session finished". - Persist committed lines so a page reload restores the transcript. - Do not poll get_transcript for this bot: streaming-only bots return 202 forever. Acceptance criteria: - Words appear in the browser within a few seconds of being spoken, and interim text never duplicates once committed. - Two browser tabs on the same meeting show the same transcript. - Ending the meeting shows the end reason, then "Session finished" on bot.done. - Optional button "Generate post-call transcript" calls POST /bots/{bot_id}/transcribe after bot.done and renders the result via the post-call flow. - Every applicable item in "Verify before you say done" passes, with evidence. ``` **Docs:** [Live transcription](/guides/transcription-recordings/live-transcription) · [Transcription providers](/guides/transcription-recordings/providers/transcription-providers) · [Set up a local webhook server](/guides/webhooks/local-webhook-server) #### 3. Google Calendar auto-join **Goal:** connect a Google Calendar once, and a bot joins every upcoming meeting that has a video link. **Human first:** create a Google Cloud OAuth client and run the refresh-token helper from the [Google Calendar OAuth setup guide](/guides/calendar-integrations/google-calendar-oauth-setup). Put `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `GOOGLE_REFRESH_TOKEN` in the environment. For signed deliveries from every calendar bot, also create a [workspace webhook endpoint](/guides/webhooks/workspace-webhooks) and store its secret as `MEETSTREAM_WEBHOOK_SECRET`. ```markdown Follow the MeetStream Agent Guide above. Build Google Calendar auto-join (Node + TypeScript). Before coding, fetch https://docs.meetstream.ai/guides/calendar-integrations/google-calendar-oauth-setup.md and use its request bodies exactly. Spec: - A setup script that connects the calendar with POST /calendar/create_calendar (google_client_id, google_client_secret, google_refresh_token from env; never log them), then confirms with GET /calendar. - GET /calendar/events: print upcoming events with their ids and meeting links. - Enable auto-join with POST /calendar/auto-schedule/enable and a default_bot_config (bot_name, video_required false). Confirm with GET /calendar/auto-schedule/settings. Only add default_bot_config fields the guide documents. If the settings don't echo a transcription provider, transcribe after the fact: on bot.done, call POST /bots/{bot_id}/transcribe with a post-call provider. - A command to schedule one event manually (POST /calendar/schedule/{event_id}) and to list scheduled bots (GET /calendar/scheduled_bots). Treat 409 on schedule as "already scheduled", not an error. Change a scheduled bot with PATCH /calendar/scheduled_bots/{bot_id} instead of re-scheduling. - Receive events on a workspace endpoint: verify X-MeetStream-Signature (sha256 HMAC of the raw body with MEETSTREAM_WEBHOOK_SECRET) and reject mismatches with 401. Acceptance criteria: - Creating a Google Calendar event with a Meet link 10 minutes out produces a scheduled bot, visible in GET /calendar/scheduled_bots. - At start time the bot joins, and after the meeting the transcript is fetched via transcript_id. - Running the setup script twice does not create duplicate bots for the same event. - A webhook with a wrong signature is rejected. A correct one is processed once. - Every applicable item in "Verify before you say done" passes, with evidence. ``` **Docs:** [Google Calendar OAuth setup](/guides/calendar-integrations/google-calendar-oauth-setup) · [Scheduling bots](/guides/features/scheduling-bots) · [Workspace webhooks](/guides/webhooks/workspace-webhooks) · [Verifying webhook signatures](/guides/webhooks/webhook-signature-verification) #### 4. MIA voice agent in a meeting **Goal:** a MeetStream Infrastructure Agent (MIA) joins a meeting, greets people, and answers questions by voice. **Human first:** MIA is bring-your-own-key. Add the model, voice, and speech-to-text provider keys you plan to use in the dashboard's [Integrations](https://app.meetstream.ai/integrations) page. ```markdown Follow the MeetStream Agent Guide above. Put a MIA voice agent into a meeting (TypeScript script, no web server needed beyond the lifecycle webhook). Before coding, fetch https://docs.meetstream.ai/guides/mia/create-an-agent.md and https://docs.meetstream.ai/guides/mia/what-is-mia.md, and only use provider ids and models they list. Spec: - Create an agent config with POST /mia: agent_name, mode "pipeline", model { provider, model, system_prompt, first_message }, voice { provider, voice_id }, transcriber { provider }. Use providers I have keys for; ask me which if unsure. - Pipeline agents gate on a wake word by default. Set wake_word { enabled: false } for an always-on agent, or tell me the wake words. - Save the returned agent_config_id and reuse it. Don't create a new config per run. - Send the agent: POST /bots/create_bot with meeting_link, bot_name, agent_config_id, and callback_url ONLY. No socket_connection_url, no live_audio_required. - Log lifecycle events and the end reason. Provide a "leave" command that calls GET /bots/{bot_id}/remove_bot. Acceptance criteria: - After I admit the bot, it speaks the first_message within a few seconds. - It answers a spoken question in character with the system prompt. - The create_bot payload contains no bridge fields (show it). - "leave" makes the bot exit, and the log shows bot.stopped then bot.done. ``` **Docs:** [What is MIA?](/guides/mia/what-is-mia) · [Create an agent](/guides/mia/create-an-agent) · [MIA API guide](/guides/mia/mia-api-guide) #### 5. Migrate from Recall.ai **Goal:** move an existing Recall.ai integration to MeetStream with a reviewed diff and a working webhook handler. ```markdown Follow the MeetStream Agent Guide above. Migrate this repository from Recall.ai to MeetStream. Before editing, fetch https://docs.meetstream.ai/migration/migrate-from-recall.md. Steps: 1. Run `npx @meetstream/migrate scan .` and show me the report. 2. Run `npx @meetstream/migrate --dry-run .` and summarize the diff by file. Wait for my go-ahead before writing anything. 3. Apply the migration, then review every file it flagged by hand. 4. Check the result against the guide's hard rules. In particular: Token auth, meeting_link, bot_name on every create, Idempotency-Key made once, and 507 as success. Webhooks move from Recall's global endpoint to a per-bot callback_url, and the handler branches terminals on bot_event (section 6). Transcripts are fetched via transcript_id, and segment text is in `transcript`. 5. Replace RECALL_API_KEY with MEETSTREAM_API_KEY in env files and examples, never with a real value. 6. Run `npx @meetstream/migrate test --api-key "$MEETSTREAM_API_KEY"`, then the project's own tests. Acceptance criteria: - No Recall base URL, header, env var, or SDK import remains (show the grep). - The project's test suite passes. - One real bot runs end to end through the migrated code path, and you report the bot_id and the events received. - A list of anything that needs a product decision rather than a code change. ``` **Docs:** [Migrate from Recall.ai](/migration/migrate-from-recall) · [Webhooks and events](/guides/webhooks/webhooks-and-events) · [Errors](/errors) #### 6. Debug a bot with the MCP **Goal:** answer "why did bot X not join?" (or "why is there no transcript?") from real data in about a minute, without writing code. **Needs:** the [action MCP](/build-with-ai/meetstream-mcp-server) connected in your agent. ```markdown Using the meetstream MCP tools, find out why bot did not join (or did not produce a transcript). Do not create, remove, or delete anything. 1. Call webhook_events_guide and use its model for everything below. 2. get_bot_status for the current status. 3. get_bot_detail: read StatusTimeline, Platform, the original RequestPayload, and transcript_id. 4. Diagnose. Common causes: - InWaitingRoom then NotAllowed: nobody admitted it before waiting_room_timeout. On Google Meet, check whether host management hides the admit prompt. - Denied: a host refused it. Nothing to fix in code. - Failed/Error: a join failure. On Zoom, check the Marketplace app and the meeting link or passcode. - Stopped early: a timeout in automatic_leave fired. Name which one. - No transcript: no provider in RequestPayload, a streaming-only provider (no post-call transcript by design), or a provider key missing from Integrations. - Joined but silent MIA agent: RequestPayload includes socket_connection_url or live_audio_required alongside agent_config_id. 5. Answer with: the root cause in one sentence, the evidence (status, timeline entries, payload fields), and the exact change to make next time. ``` **What good looks like:** "Bot `…` was never admitted. The timeline shows `InWaitingRoom` for 600 seconds, then `NotAllowed`. Admit bots faster, or use a signed-in bot so it isn't held as an anonymous guest." **Docs:** [Debugging bots](/guides/help/debugging-bots) · [Google Meet lobby and admission](/guides/app-integrations/gmeet-lobby-admission) · [Automatic leave configuration](/guides/features/automatic-leave-configuration) ## Why this is structured the way it is #### One guide, no copies The rules live in one block. Quickstarts reference it instead of repeating it, so fixing a rule once fixes it everywhere and your agent's context stays small. #### Human steps are explicit Keys, tunnels, Zoom apps, and lobby admission are things only a person can do. The guide tells the agent to stop and ask for them, then verify the result. #### Done means verified Every quickstart ends in acceptance criteria, and the guide ends in a checklist that needs evidence: status codes, event names, IDs. #### Built from production data The webhook model reflects thousands of captured production deliveries, including the two-layer terminal and `bot.done` as the final event on every path. ## Next steps * [Docs for Agents & LLMs](/build-with-ai/docs-for-agents): the full machine-readable surface * [Create Bot payload reference](/api-reference/create-bot-payload-reference): every create\_bot field * [Errors](/errors): every status code and what to do about it > Meeting bot API documentation for Zoom, Google Meet and Microsoft Teams: create bots, stream real-time audio, transcribe, and run in-meeting voice agents.