Meetstream
Core MeetStream API reference, lifecycle, decision tree, and live-verified endpoint catalog. Auto-activate on any mention of MeetStream, meeting bots, meeting recording API, meeting transcription API, real-time transcription, live captions, speaker diarization, AI meeting summarizer, conversation intelligence, AI meeting agent, or any of these intents: "build me a notetaker", "build a meeting bot", "record a Zoom meeting", "transcribe Google Meet", "join a meeting programmatically", "Microsoft Teams bot", "AI sales coach", "live meeting captions", "meeting webhook", "meeting recap email", "meeting action items extractor", "send a bot to a meeting", "Google Calendar auto-record". Also activate when the user references meeting bot API errors, MeetStream webhook events (bot.joining, bot.recording, bot.stopped, transcription.processed, etc.), or asks how to integrate with api.meetstream.ai. For specific scaffolds (notetaker, sales coach, calendar automation) prefer the dedicated use-case skills which chain back to this one. When the user asks to BUILD or CREATE, always enter plan mode first using the superpowers:writing-plans skill before writing any code.From its SKILL.md
npx -y skills add meetstream-ai/claude-plugin --skill meetstreamAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
76.6 KB, as published. Nobody here has run it
MeetStream Developer Skill
You are a MeetStream integration expert. Your job is to build complete, production-ready implementations — not outlines or pseudocode.
Source of truth: every endpoint, field, and payload in this skill is verified against
docs.meetstream.ai. If you need something that isn't here, fetch it fromhttps://docs.meetstream.ai/<path>.md(append.mdto any docs page URL for clean markdown) or via the MCP server athttps://docs.meetstream.ai/_mcp/server. Do not invent endpoints or fields.
Step 0: Before Writing Any Code
If the user asks you to BUILD, CREATE, IMPLEMENT, or SET UP a MeetStream integration:
- Invoke the
superpowers:writing-plansskill first - Create the project folder if it doesn't exist
- Read
references/code-patterns-node.mdorreferences/code-patterns-python.mdfor the relevant patterns - Write the full plan to
docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md - Offer subagent-driven vs inline execution
Skip plan mode only if the user explicitly says "quick snippet", "just show me", or "skip planning".
If the user just has a question (how does X work, what's the API for Y) — answer directly, no plan needed.
Setup
All MeetStream API calls need this header:
Authorization: Token YOUR_API_KEY
API keys are created at https://app.meetstream.ai/api-keys.
First-time users — invoke getting-started skill BEFORE anything else
If MEETSTREAM_API_KEY is missing from the user's environment, OR if a quick curl -H "Authorization: Token $MEETSTREAM_API_KEY" https://api.meetstream.ai/api/v1/bots returns non-200, do not attempt to scaffold or call any endpoint. Invoke the getting-started skill — it walks the user through:
- Signup at https://app.meetstream.ai (free tier, no credit card)
- API key creation at https://app.meetstream.ai/api-keys
- Environment variable setup (
export MEETSTREAM_API_KEY=ms_xxxxx+.zshrc/.bashrc/.env) - Validation ping
- Optional handoff to
verify-accountto confirm transcription providers are configured
Then resume the original request. Skip this only if the user explicitly says "I already have my key set."
Base URL: https://api.meetstream.ai/api/v1 (no trailing slash in canonical examples).
Default language: Python unless the user specifies otherwise.
Gathering Requirements
Before planning or building, walk the user through the Bot Configuration Decision Tree section below in order. Don't just ask the 4 top-level questions and guess defaults for everything else — the decision tree covers every field that affects behavior and cost.
Quick top-level questions to start with:
- API key — if not provided, ask. Link: https://app.meetstream.ai/api-keys
- Platform — Google Meet, Teams, Zoom, or all? (Zoom needs extra setup; see Platform Notes)
- Language — Python or Node.js/TypeScript?
- Use case — match to one of the patterns below
Then walk Steps 1–8 of the decision tree to fill in:
- Bot identity (name, avatar, message, video/audio config) — Step 1
- Platform-specific setup (Google signed-in, Zoom OBF) — Step 2
- Transcription strategy (none / live / post-call / both) — Step 3
- Live transcription provider + webhooks — Step 4A
- Post-call transcription provider — Step 4B
- Hybrid (live + post-call via
/transcribe) — Step 4C - Participant events, interactive bot, MIA — Step 5
- Scheduling (now / future / calendar auto) — Step 6
- Custom metadata for routing/billing — Step 7
- Timeouts — Step 8
Each step's branch produces one or more fields in the final create_bot payload. Don't guess defaults — ask.
Required Fields on create_bot
Per the OpenAPI CreateBotRequest schema, only two fields are required:
meeting_link(string) — Zoom, Google Meet, or Teams URLbot_name(string) — the display name shown in the meeting
video_required defaults to true (the bot records video unless you opt out). Set video_required: false for transcript-only workflows.
There is no audio_required field on CreateBotRequest. Audio is recorded by default. (audio_separate_streams is a different feature that enables per-participant audio tracks.)
Note:
audio_requiredDOES exist for calendar-scheduled bots — insidebot_configonPOST /calendar/schedule/{event_id}, the auto-scheduledefault_bot_config, etc. The "doesn't exist" rule only applies tocreate_bot.
Recommended Implementation Strategy
Pick the pattern that matches what the user actually needs. The first two are the primary workflows; the third is a fallback for specific cases.
Primary patterns
| Need | What to configure | Lifecycle terminal event | Where the transcript lives |
|---|---|---|---|
| Post-call transcript (notetaker, recordings, summaries) — the most common use case | recording_config.transcript.provider set to a post-call provider (deepgram / assemblyai / sarvam / jigsawstack / meetstream) + a callback_url for webhooks | bot.done | GET /transcript/{tid}/get_transcript after transcription.processed fires — fetched via the canonical bot_details.transcript_id flow |
| Live transcript chunks during the meeting (AI coaching, live captions, real-time agents) | live_transcription_required.webhook_url + a streaming provider in recording_config.transcript.provider (meetstream_streaming works free on stock accounts; *_streaming for external providers) | audio.processed (no bot.done for streaming-only bots) | Each chunk POSTed to your webhook in real time; nothing is saved server-side for post-call fetch |
For the standard post-call notetaker workflow (95% of integrations):
create_botwithrecording_config.transcript.provider: {deepgram: {...}}(or assemblyai, etc.) andcallback_url- Wait for the
transcription.processedwebhook GET /bots/{bot_id}/detail→ readbot_details.transcript_idGET /transcript/{transcript_id}/get_transcript→ top-level array of segments
No /transcribe call needed. The bot was configured with a post-call provider up front, so the transcript is generated automatically.
Backup / fallback pattern (POST /bots/{bot_id}/transcribe)
/transcribe is not the primary path — it's a backup option for these specific cases:
- You used a streaming-only provider at meeting time (live transcription) but now want a post-call transcript too. Audio is saved, so you can transcribe on demand.
- The original post-call provider failed (e.g.
transcription.failedbecause credits ran out). Retry with a different provider that does have credit, without re-running the meeting. - You want to re-transcribe with a higher-quality / different-language provider after the fact.
In all other cases, configure the right provider on create_bot up front. /transcribe is a recovery tool, not a default.
Bot Configuration Decision Tree
Walk the user through these questions in order. Each answer pins down one field in the final create_bot payload. At the end, you'll have a complete, correct request body tailored to their use case.
When the user is vague ("I want a meeting bot"), DO NOT guess defaults — actually walk them through this tree. The defaults are not always what they want (e.g.
video_requireddefaults totruewhich burns storage for transcript-only use cases).
STEP 1 — Bot identity (who is this bot in the meeting?)
Q1.1: What should the bot be called inside the meeting? [REQUIRED]
└─→ "bot_name": "Acme Notetaker" (visible to all participants)
Q1.2: Should it post a welcome message in the meeting chat on join?
├─ Yes → "bot_message": "Hi! I'm an AI notetaker recording this call."
└─ No → omit bot_message
Q1.3: Custom avatar / profile picture?
├─ Yes → "bot_image_url": "https://your-cdn.com/avatar.png"
│ • MUST be publicly accessible URL (no auth, no signed URLs)
│ • Base64 / data URIs NOT supported
│ • Host it on S3 / Cloudflare R2 / public CDN
└─ No → omit bot_image_url
Q1.4: Will you ever need the meeting video file later (archive, playback)?
├─ Yes → "video_required": true (DEFAULT — bot records video)
└─ No → "video_required": false (saves storage + bandwidth; audio still captured)
Q1.5: Want per-participant audio tracks (one file per speaker)?
├─ Yes → "audio_separate_streams": true
│ • Google Meet + Zoom only (NOT Teams)
│ • Up to 16 concurrent speakers
│ • Each track: WebM/Opus, 48kbps, 48kHz mono
└─ No → omit (default false; you get one mixed audio file)
Q1.6: Want per-participant video tracks (one webcam stream per person)?
├─ Yes → "video_separate_streams": true
│ • All 3 platforms
│ • Up to 6 concurrent webcams
│ • WebM/VP8, 15 FPS, video-only
└─ No → omit (you get one composite video)
❌ Do NOT add
"audio_required": true— that field doesn't exist oncreate_bot(audio is always captured). It DOES exist in calendar-scheduledbot_config, which is a different schema.
STEP 2 — Meeting platform
Q2.1: Which platform is the meeting on?
├─ Google Meet → "meeting_link": "https://meet.google.com/abc-defg-hij"
│ continue Q2.2 (signed-in?)
├─ Microsoft Teams → "meeting_link": "https://teams.microsoft.com/l/meetup-join/..."
│ (no extra config needed)
└─ Zoom → "meeting_link": "https://zoom.us/j/123456789?pwd=..."
continue Q2.3 (Zoom setup)
Q2.2: (Google Meet) Does the meeting require a signed-in Google identity?
├─ Yes → "google_meet": {
│ "login_required": true,
│ "google_login_domain": "your-company.com"
│ }
│ ⚠ Prerequisite: SSO + login cert registration via
│ /google-login-domains and /google-logins. See Google Signed-In
│ Bots section. Without this setup the bot will fail to join.
└─ No → omit google_meet (works for public Meet rooms)
Q2.3: (Zoom) Have you registered a Zoom app for production use?
├─ Yes, with OBF (On-Behalf-Of) flow
│ → "zoom": { "use_zoom_obf": true }
│ Prerequisite: Zoom dashboard setup at
│ https://docs.meetstream.ai/guides/app-integrations/zoom-marketplace-app-setup
├─ Yes, standard production
│ → omit zoom field (default flow)
└─ Dev mode (no submission yet)
→ omit zoom field
⚠ Dev mode restricts the bot to meetings hosted by the app owner.
For external customer meetings, submit the Zoom app to production.
STEP 3 — Do you need a transcript? (this is the biggest decision)
Q3.1: Will any part of the system consume a transcript?
├─ No (audio-only, e.g. recording for human listening later)
│ → Skip Steps 4 & 5 entirely. You can still call /transcribe later
│ if you change your mind (audio is saved either way).
└─ Yes → continue Q3.2
Q3.2: WHEN do you need the transcript?
├─ During the meeting (live captions, real-time AI coach, live dashboard)
│ → STEP 4A (LIVE)
├─ After the meeting (summaries, CRM updates, archives)
│ → STEP 4B (POST-CALL)
└─ Both (live captions in the UI + complete transcript post-call)
→ STEP 4C (HYBRID — use streaming live + /transcribe after)
STEP 4A — Live transcription path
Q4A.1: Where should live chunks be POSTed?
→ "live_transcription_required": { "webhook_url": "https://you/live" }
(HTTPS, NOT a WebSocket; bot POSTs JSON chunks here every ~250ms)
Q4A.2: Which streaming provider?
├─ "I don't have any provider API keys" (most accounts)
│ → "recording_config": {
│ "transcript": { "provider": { "meetstream_streaming": {} } }
│ }
│ ✅ Free, no external key needed, works out of the box
│ ✅ Returns speaker names from the meeting
│ ❌ confidence/speech_confidence values are 0 (no per-word ML scores)
│
├─ "I have a Deepgram account" (best for accuracy)
│ → provider: {
│ "deepgram_streaming": {
│ "transcription_mode": "sentence",
│ "model": "nova-2",
│ "language": "en",
│ "punctuate": true,
│ "smart_format": true,
│ "endpointing": 300,
│ "vad_events": true,
│ "utterance_end_ms": 1000,
│ "encoding": "linear16",
│ "channels": 1
│ }
│ }
│ ⚠ ALL fields required per OpenAPI. Account needs Deepgram key.
│
├─ "I have an AssemblyAI account"
│ → provider: {
│ "assemblyai_streaming": {
│ "transcription_mode": "raw",
│ "sample_rate": 48000,
│ "speech_model": "universal-streaming-english",
│ "format_turns": false,
│ "encoding": "pcm_s16le",
│ "vad_threshold": "0.4",
│ "end_of_turn_confidence_threshold": "0.4",
│ "inactivity_timeout": 300,
│ "min_end_of_turn_silence_when_confident": "400",
│ "max_turn_silence": "1280"
│ }
│ }
│ ⚠ English-only. Account needs AssemblyAI key.
│
├─ "I have a JigsawStack account"
│ → provider: { "jigsawstack_streaming": {} }
│ Account needs JigsawStack key.
│
└─ "Just use the meeting's built-in captions" (Google Meet / Teams only)
→ provider: { "meeting_captions": {} }
✅ Free, no provider needed at all
❌ Quality depends on the meeting platform's own captions
❌ Doesn't work on Zoom
Q4A.3: Need to also stream raw audio bytes (for your own ML model)?
├─ Yes → "live_audio_required": { "websocket_url": "wss://you/audio" }
│ See Pattern 7 — binary frames over WebSocket, raw PCM16
│ LE @ 48kHz mono with embedded speaker metadata.
└─ No → omit live_audio_required
Q4A.4: Need to also stream live video (fMP4)?
├─ Yes → "live_video_required": { "websocket_url": "wss://you/video" }
│ ⚠ Google Meet + Teams only (NOT Zoom)
│ See Pattern 8 — fMP4 over WebSocket with ping/pong contract.
└─ No → omit live_video_required
⚠ KEY LIMITATION OF STREAMING: No automatic post-call transcript.
transcript_id will be null in the create_bot response.
bot_details.transcript_id stays null.
/transcriptions list is empty.
You MUST manually call POST /bots/{bot_id}/transcribe after the meeting
if you also want a post-call transcript (that's Step 4C).
STEP 4B — Post-call transcription path
Q4B.1: Where should webhooks be delivered? [STRONGLY RECOMMENDED]
→ "callback_url": "https://you/webhook"
You'll receive bot.joining → bot.inmeeting → bot.recording → bot.leaving
→ bot.stopped → manifest.completed → audio.processed →
transcription.processed → bot.done (full Path A lifecycle).
Without callback_url you'd have to poll /bots/{id}/status repeatedly.
Q4B.2: Which post-call provider?
├─ "I don't have external keys, want lowest setup"
│ → "recording_config": {
│ "transcript": {
│ "provider": {
│ "meetstream": { "language": "en", "translate": false }
│ }
│ }
│ }
│ MeetStream in-house post-call. May require provider key in account.
│
├─ "I want highest accuracy English" (most popular choice)
│ → provider: { "deepgram": { "model": "nova-3", "language": "en" } }
│ ⚠ Account needs Deepgram key. nova-3 is the only model enum.
│
├─ "I want speaker diarization + PII redaction"
│ → provider: {
│ "assemblyai": {
│ "speech_models": ["universal-2"],
│ "language_code": "en_us",
│ "speaker_labels": true,
│ "punctuate": true,
│ "format_text": true,
│ "filter_profanity": false,
│ "redact_pii": false,
│ "auto_chapters": false,
│ "entity_detection": false
│ }
│ }
│ ⚠ 9 fields marked required per OpenAPI. Account needs AssemblyAI key.
│
├─ "Indic languages (Hindi/Tamil/Telugu/etc.)"
│ → provider: {
│ "sarvam": {
│ "model": "saaras:v3",
│ "language_code": "en-IN",
│ "mode": "transcribe",
│ "with_diarization": true
│ }
│ }
│ Account needs Sarvam key.
│
└─ "Auto-detect language + translate to English"
→ provider: {
"jigsawstack": {
"language": "auto",
"translate": false,
"by_speaker": true
}
}
Account needs JigsawStack key.
Q4B.3: How long should MeetStream keep the audio/transcript?
├─ 24 hours (default if you omit retention)
├─ Custom window → "retention": { "type": "timed", "hours": 168 } (7 days)
└─ Per compliance policy → set the smallest window that meets your needs.
After that, data is auto-deleted and
data_deletion webhook fires.
✅ The transcript will be auto-fetched-able via the canonical flow:
1. Wait for transcription.processed webhook
2. GET /bots/{bot_id}/detail → bot_details.transcript_id
3. GET /transcript/{transcript_id}/get_transcript
No /transcribe call needed (Path A, the standard workflow).
STEP 4C — Hybrid (live + post-call)
Configure the bot for LIVE only at create time (Step 4A), then:
After you receive bot.stopped, call:
POST /bots/{bot_id}/transcribe
{
"provider": {"deepgram": {...}}, # any post-call provider
"callback_url": "https://you/webhook" # gets transcription.processed/.failed
}
This:
- Returns a new transcript_id
- Overwrites bot_details.transcript_id (canonical fetch returns this one)
- Fires exactly ONE transcription.processed or .failed (no bot.done follow-up)
- Does NOT include custom_attributes in the /transcribe webhook
STEP 5 — Other webhooks (beyond transcription)
Q5.1: Need to know when participants join/leave the meeting?
├─ Yes → add to recording_config:
│ "realtime_endpoints": [{
│ "type": "webhook",
│ "url": "https://you/participants",
│ "events": ["participant_events.join", "participant_events.leave"]
│ }]
│ ⚠ These webhooks have a DIFFERENT shape — nested under
│ data.data.action / data.bot.id, no top-level bot_id or bot_status.
└─ No → omit realtime_endpoints
Q5.2: Want your bot to interact in the meeting (send chat, play TTS audio,
change its video frame)?
├─ Yes → "socket_connection_url": { "websocket_url": "wss://you/control" }
│ See Pattern 3 — bot opens a WS to your server; you send the
│ 5 documented commands (sendaudio, sendmsg, sendchat, interrupt,
│ sendimg/sendimg_url) over it.
└─ No → omit socket_connection_url
Q5.3: Want to attach a MeetStream Infrastructure Agent (MIA) to the bot?
├─ Yes → Create the agent in the dashboard (app.meetstream.ai) or via POST /api/v1/mia
│ (see Pattern 9), then pass only its ID on create_bot:
│ "agent_config_id": "<from above>"
└─ No → omit agent_config_id
STEP 6 — Scheduling
Q6.1: When does the bot join?
├─ Right now → omit join_at; bot joins as soon as create_bot returns
├─ At a specific time → "join_at": "2026-04-22T15:00:00Z" (ISO-8601)
└─ Automatically for all calendar meetings →
Skip create_bot. Use the Calendar API instead:
POST /calendar/create_calendar (connect)
POST /calendar/auto-schedule/enable (with default_bot_config)
See "Pattern 6: Calendar Auto-Scheduling".
STEP 7 — Metadata for routing/billing
Q7.1: Need to correlate webhooks back to your internal records (tenant ID,
deal ID, user ID)?
├─ Yes → "custom_attributes": {
│ "tenant_id": "acme-123",
│ "deal_id": "hubspot-456",
│ "internal_user_id": "u_789"
│ }
│ ⚠ Stringify all values defensively (live API has been observed
│ to reject non-string values even though OpenAPI allows any).
│ ⚠ custom_attributes is echoed on lifecycle events BUT NOT on
│ /transcribe-triggered webhooks. Correlate by bot_id there.
└─ No → omit custom_attributes (recommended only for trivial use cases)
STEP 8 — Timeouts (use these defaults unless you have a reason not to)
"automatic_leave": {
"waiting_room_timeout": 600, // 10 min — long meetings start late
"everyone_left_timeout": 600, // 10 min — handle bio breaks
"voice_inactivity_timeout": 600, // 10 min — silent presentations
"in_call_recording_timeout": 14400, // 4 hr — handle long workshops
"recording_permission_denied_timeout": 300 // 5 min — Zoom-only, MAX is 300
}
Live-verified constraints:
in_call_recording_timeoutminimum 600s (API HTTP 400 below)recording_permission_denied_timeoutrange 60–300s (API HTTP 400 outside)
Final assembly — your complete create_bot payload
Combine every answer above. A realistic post-call notetaker for Google Meet might look like:
POST https://api.meetstream.ai/api/v1/bots/create_bot
Authorization: Token YOUR_API_KEY
{
"meeting_link": "https://meet.google.com/abc-defg-hij",
"bot_name": "Acme Notetaker",
"bot_message": "Hi! I'm an AI notetaker for Acme. Transcript will be emailed after.",
"bot_image_url": "https://acme-cdn.com/bot-avatar.png",
"video_required": false,
"callback_url": "https://api.acme.com/webhooks/meetstream",
"custom_attributes": {
"tenant_id": "acme-prod-tenant-42",
"user_id": "u_12345"
},
"recording_config": {
"transcript": {
"provider": { "deepgram": { "model": "nova-3", "language": "en" } }
},
"retention": { "type": "timed", "hours": 168 }
},
"automatic_leave": {
"waiting_room_timeout": 600,
"everyone_left_timeout": 600,
"voice_inactivity_timeout": 600,
"in_call_recording_timeout": 14400,
"recording_permission_denied_timeout": 300
}
}
A live AI-coach bot for the same meeting might look like:
{
"meeting_link": "https://meet.google.com/abc-defg-hij",
"bot_name": "Sales Coach",
"video_required": false,
"callback_url": "https://api.acme.com/webhooks/meetstream",
"live_transcription_required": {
"webhook_url": "https://api.acme.com/live/transcript"
},
"recording_config": {
"transcript": { "provider": { "meetstream_streaming": {} } },
"retention": { "type": "timed", "hours": 24 }
},
"automatic_leave": {
"waiting_room_timeout": 600,
"everyone_left_timeout": 600,
"voice_inactivity_timeout": 600,
"in_call_recording_timeout": 14400,
"recording_permission_denied_timeout": 300
}
}
When in doubt, walk the user through every step above before sending create_bot. It's better to ask 3 extra questions than to ship a bot configured wrong for their use case.
Power Prompts (one-shot scaffolds)
When a user asks for any of these common products, treat the prompt as a complete spec and scaffold the entire app — webhook server, MeetStream integration, AI processing, delivery layer, error handling — in one pass. Hand off to the relevant use-case skill (notetaker, sales-coach, calendar-automation, etc.) for the actual scaffolding, but recognize these patterns as "build the whole thing now" signals:
- API-triggered notetaker — POST
/meetings/start→ join → summarize → email all attendees + trigger user via Resend (usenotetakerskill) - Real-time sales coach — meeting link → live transcription → LLM detection → WebSocket push to browser (use
sales-coachskill) - Calendar auto-recording — Google Calendar OAuth → auto-schedule → summary delivery (use
calendar-automation+notetakerskills) - CRM auto-enricher (HubSpot/Salesforce) — post-call → extract action items + signals → upsert CRM activity (use
notetakerskill + CRM SDK) - Interview recorder for HR/recruiting — per-participant audio + structured interview report (use
notetakerskill withaudio_separate_streams: true) - Multi-tenant notetaker SaaS — per-user OAuth, per-user calendars, billing-ready (use
calendar-automation+notetakerskills with tenant-scoped routing) - Compliance recorder — 90-day retention + per-participant audio + audit log + KMS-encrypted transcripts (use
notetakerskill withretention.hours: 2160+ audit logging) - AI Meeting Assistant (MIA) — voice-interactive bot that joins and participates (use MIA pattern in
meetstreamcore skill) - Live captions broadcaster — accessibility-focused live transcript via SSE (use
sales-coachskill structure but render captions instead of cards) - Customer call analyzer + insights dashboard — structured extraction + week-over-week analytics (use
notetakerskill + Postgres + React)
Each is a complete spec — don't ask 4 clarifying questions if the user pasted one of these patterns. Pre-pick sensible defaults (deepgram for post-call, meetstream_streaming for live, Resend for email, Redis for dedup) and just build. Surface a "want to change any of these defaults?" prompt AFTER the scaffold, not before.
For the full copy-paste prompts (developers can paste them directly into Claude Code), see the "Power Prompts" section in the plugin's README.md.
Use Case Patterns
Map the user's request to one of these, then read the relevant code pattern file for the full implementation.
Pattern 1: Record + Transcript (most common)
Bot joins, records audio, transcript fetched post-meeting.
POST /bots/create_bot
{
"meeting_link": "https://zoom.us/j/123456789",
"bot_name": "Recorder",
"video_required": false,
"callback_url": "https://your-server.com/webhook",
"recording_config": {
"transcript": {
"provider": { "deepgram": { "language": "en", "model": "nova-3" } }
},
"retention": { "type": "timed", "hours": 48 }
}
}
Returns HTTP 201 Created with BotResponse:
{
"bot_id": "...",
"transcript_id": "..." | null,
"meeting_url": "...",
"status": "Active"
}
How to fetch the transcript — canonical pattern:
- Wait for the
transcription.processedwebhook (signals readiness — but does not containtranscript_id) - Call
GET /bots/{bot_id}/detail→ readbot_details.transcript_id - Call
GET /transcript/{transcript_id}/get_transcript
This is stateless — no need to store transcript_id from create_bot. The webhook gives you bot_id; you derive the rest.
# The whole flow, after the transcription.processed webhook fires:
detail = requests.get(f"{BASE}/bots/{bot_id}/detail", headers=H).json()
tid = detail["bot_details"]["transcript_id"]
resp = requests.get(f"{BASE}/transcript/{tid}/get_transcript", headers=H)
if resp.status_code == 200:
segments = resp.json() # ← list (top-level array)
elif resp.status_code == 202:
msg = resp.json() # ← dict: {"message": "Transcript is still being processed. ..."}
# transcript not ready yet — back off and retry, or wait for next webhook
Live-verified response shapes from
/transcript/{tid}/get_transcript:
- HTTP 200 → top-level JSON array of segment objects (success case)
- HTTP 202 → JSON object
{"message": "Transcript is still being processed. Current status: <state>"}— transcript_id exists but isn't ready. Always check status code before iterating.- Even if the
transcription.processedwebhook never fires (e.g. provider auth failure →transcription.failed),bot_details.transcript_idis still populated, but the fetch will return 202 indefinitely — it does NOT switch to an error response. A blind retry loop hangs forever.Three status indicators disagree on failure (live-verified, treat the first as authoritative):
Source Value on transcript failure bot_details.TranscriptStatus"Failed"← authoritative/transcriptions[].status"Not started"(stale)/transcript/{tid}/get_transcriptHTTP202foreverAlways check
bot_details.TranscriptStatusbefore retrying, or watch for thetranscription.failedwebhook to break out of the polling loop:def fetch_transcript_safely(bot_id: str, max_polls: int = 30): for _ in range(max_polls): detail = requests.get(f"{BASE}/bots/{bot_id}/detail", headers=H).json() ts = detail["bot_details"].get("TranscriptStatus") if ts == "Failed": raise RuntimeError(f"Transcript failed for {bot_id}") if ts == "Success": tid = detail["bot_details"]["transcript_id"] resp = requests.get(f"{BASE}/transcript/{tid}/get_transcript", headers=H) if resp.status_code == 200: return resp.json() # top-level array time.sleep(2) raise TimeoutError("Transcript not ready")
Where transcript_id lives — all four live-verified sources:
| Source | When to use |
|---|---|
bot_details.transcript_id on GET /bots/{bot_id}/detail | Canonical / default path. Stateless — bot_id from webhook is all you need. |
BotResponse.transcript_id in create_bot response | Available immediately on bot creation if you want to log it; not required. |
GET /bots/{bot_id}/transcriptions | Only when you've called POST /bots/{bot_id}/transcribe to re-transcribe with a different provider and need to list all runs. |
POST /bots/{bot_id}/transcribe response | When triggering a fresh transcription run. |
For provider meeting_captions, all four give transcript_id: null — fetch the S3 caption file from bot_details.caption_file on /detail instead.
transcript_id is NOT in any webhook payload. The transcription.processed webhook contains only bot_id, event, transcript_status, message, status_code. Use the canonical /detail pattern above to resolve it.
Once you receive the transcription.processed webhook, fetch:
GET /transcript/{transcript_id}/get_transcript
The response is a top-level JSON array of segment objects:
[
{
"speaker": "Alice",
"transcript": "Hello everyone, let's get started.",
"start_time": 0.5,
"end_time": 3.2,
"absolute_start_time": "2026-04-22T10:00:00.500Z",
"absolute_end_time": "2026-04-22T10:00:03.200Z",
"words": [{ "word": "Hello", "punctuated_word": "Hello,", "start": 0.5, "end": 0.8, "confidence": 0.99, "speaker": 0, "speaker_confidence": 0.95 }]
}
]
The per-segment text field is transcript, not text. The response is the array itself, not {transcript: [...]}.
For raw provider output (unformatted), add ?raw=true.
Pattern 2: Real-Time Transcription (AI agents, live coaching)
Live transcription is delivered via HTTPS webhook (POST) — not WebSocket.
Live-verified prerequisite:
live_transcription_required.webhook_urlREQUIRES a streaming provider inrecording_config.transcript.provider. The live API returnsHTTP 400: "live_transcription_required.webhook_url is provided but no streaming provider found. Please provide at least one streaming provider (deepgram_streaming, assemblyai_streaming, jigsawstack_streaming, meetstream_streaming, or meeting_captions)."if you set the webhook URL without one.
Five known streaming providers (live-verified from server error message):
deepgram_streaming(requires external Deepgram API key in your MeetStream account)assemblyai_streaming(requires external AssemblyAI API key)jigsawstack_streamingmeetstream_streaming— MeetStream in-house, no external key required (works on stock accounts)meeting_captions— Google Meet / Teams native captions (free)
{
"live_transcription_required": { "webhook_url": "https://your-server.com/live-transcript" },
"recording_config": {
"transcript": {
"provider": {
"deepgram_streaming": {
"transcription_mode": "sentence",
"model": "nova-2",
"language": "en",
"punctuate": true,
"smart_format": true,
"endpointing": 300,
"vad_events": true,
"utterance_end_ms": 1000,
"encoding": "linear16",
"channels": 1
}
}
}
}
}
For accounts without external provider keys, swap in meetstream_streaming: {} — empty config is accepted.
Note: Streaming-only providers (meetstream_streaming, meeting_captions, the _streaming variants) do NOT produce a post-call transcript via /transcript/{tid}/get_transcript. create_bot returns transcript_id: null (for meeting_captions) or a transcript_id that fetch returns HTTP 202 forever (for meetstream_streaming). Use the live webhook stream as the canonical record — don't expect a post-call fetch.
Each POST to your webhook_url:
{
"bot_id": "305e708e-...",
"speakerName": "Alice",
"timestamp": "2026-05-26T10:21:43.681Z",
"transcript": "Can you walk me through",
"utterance": "",
"words": [{ "word": "Can", "start": 0.2, "end": 0.5, "confidence": 0.99, "speaker": "Alice", "punctuated_word": "Can", "speech_confidence": 0.99 }],
"is_final": false,
"end_of_turn": false,
"turn_is_formatted": false,
"transcription_mode": "raw",
"custom_attributes": {}
}
For raw live audio bytes (binary frames over WebSocket), see Pattern 7 below. For raw live video (fMP4 over WebSocket), see Pattern 8.
Pattern 3: Interactive Bot (chat, TTS audio, dynamic video frame)
Bot opens a WebSocket back to your server for two-way control:
{ "socket_connection_url": { "websocket_url": "wss://your-server.com/bot-control" } }
On join the bot sends a JSON handshake:
{ "type": "ready", "bot_id": "...", "message": "Ready to receive messages" }
Then you push JSON commands over that WebSocket. There are five documented commands:
| Command | Purpose |
|---|---|
sendaudio | Play raw PCM16 LE audio (48 kHz mono, base64-encoded) through the bot's mic |
sendmsg | Post a chat message (requires BOTH message AND msg fields with the same value, for cross-platform compat) |
sendchat | Chat with role tagging + streaming support (role, text, is_final) |
interrupt | Stop bot audio playback ({action: "clear_audio_queue"}) — Google Meet only; Zoom/Teams accept but don't clear |
sendimg / sendimg_url | Set bot's video frame to an image (base64 via img, or URL via img_url) |
For chat / images into the chat panel only (not video frame), there are also REST endpoints:
POST /bots/{bot_id}/send_message body: { "message": "...", "metadata": { "message_type": "text" } }
POST /bots/{bot_id}/send_image body: { "img_url": "https://...", "display_duration": 5, "metadata": {...} }
POST /bots/{bot_id}/pause_recording (empty body — suspend recording mid-meeting; bot stays in the call)
POST /bots/{bot_id}/resume_recording (empty body — resume a paused recording)
send_imagefield isimg_url(notimage_url).display_duration(integer seconds) is optional.
See references/code-patterns-python.md Pattern 3 and references/code-patterns-node.md Pattern 3 for full implementations of every command including chunked audio streaming, barge-in, and streaming chat.
Pattern 4: Calendar-Automated Bot
Connect Google Calendar so MeetStream auto-handles event sync, scheduling, recurring events, change detection, and watch channel renewal.
Connect:
POST /calendar/create_calendar
{
"google_refresh_token": "...",
"google_client_id": "...",
"google_client_secret": "..."
}
Field names use the google_ prefix. All three are required. Plain refresh_token / client_id / client_secret will be rejected by the OpenAPI schema.
Endpoints (full list from docs):
| Method | Path | Purpose |
|---|---|---|
| POST | /calendar/create_calendar | Connect a Google Calendar |
| GET | /calendar/calendars | List connected calendars (live from Google) |
| GET | /calendar/events | Sync + list events with pagination + bot info |
| GET | /calendar/get_events | List events from local DB only (faster, no Google call) |
| POST | /calendar/schedule/{event_id} | Schedule a bot for an event (body: bot_config, occurrence_date, schedule_all_occurrences, occurrence_limit, recurring_event) |
| DELETE | /calendar/schedule/{event_id} | Unschedule an event (body: cancel_all_occurrences, from_date) |
| GET | /calendar/scheduled_bots | List all upcoming scheduled bots |
| PATCH | /calendar/scheduled_bots/{bot_id} | Update a scheduled bot (scheduled_join_time, bot_username, custom_attributes) |
| DELETE | /calendar/scheduled_bots/{bot_id} | Delete a single scheduled bot |
| POST | /calendar/auto-schedule/enable | Enable auto-scheduling (body: default_bot_config) |
| POST | /calendar/auto-schedule/disable | Disable auto-scheduling |
| GET | /calendar/auto-schedule/settings | Read current auto-schedule settings |
| POST | /calendar/auto-reschedule | Trigger reschedule for next recurring occurrence |
| POST | /calendar/toggle-recurrence | Enable/disable auto-rescheduling per event (body: {event_id, recurring_enabled}) |
Disconnect: use
DELETE /calendar/disconnect— that's what the docs guide, cURL example, and response example all show. Returns{disconnected, user_id, watch_channel_stopped, events_deleted, schedules_cancelled, message}. Irreversible — stops watch channels, cancels pending schedules, deletes synced events, removes Google OAuth credentials. (The OpenAPI spec shows POST as a quirk; disregard.)
Auto-scheduling behavior: job runs every 24h at midnight UTC, schedules bots for the next 24h, joins 1 minute before meeting start. Real-time push notifications from Google Calendar handle reschedules and cancellations. Watch channels auto-renew every ~6 days (a daily background job renews any expiring within 2 days).
Recurring events / RRULE: standard iCalendar rules supported (DAILY/WEEKLY/MONTHLY, BYDAY, COUNT, UNTIL). Schedule with recurring_event: true to auto-reschedule after each occurrence, or schedule_all_occurrences: true (with optional occurrence_limit, default 52) to batch-schedule.
Deduplication: scheduling the same event twice returns HTTP 409 with the existing bot's ID. Use PATCH /calendar/scheduled_bots/{bot_id} to update.
Pattern 5: Note-Taker (full pipeline)
Webhook server + bot creation + transcript fetch + AI summary + email delivery.
See references/code-patterns-node.md Pattern 4 (Next.js) or references/code-patterns-python.md Pattern 5 (Flask + OpenAI). Build plan should include: webhook handler (returns 200 fast, idempotent), transcript fetch via transcript_id, LLM summary, delivery layer.
Pattern 6: Participant Tracking (real-time)
Real-time join/leave events during the meeting:
{
"recording_config": {
"realtime_endpoints": [{
"type": "webhook",
"url": "https://your-server.com/participants",
"events": ["participant_events.join", "participant_events.leave"]
}]
}
}
Pattern 7: Live Audio Streaming (binary frames)
Real-time PCM audio with embedded speaker metadata over a WebSocket you host:
{ "live_audio_required": { "websocket_url": "wss://your-server.com/audio" } }
The bot connects to your WSS, sends a JSON ready handshake, then streams binary frames with this exact wire format:
┌──────────┬────────────┬────────────┬──────────────┬──────────────┬──────────────────┐
│ msg_type │ sid_length │ speaker_id │ sname_length │ speaker_name │ pcm_audio_data │
│ 1 byte │ 2 bytes │ L1 bytes │ 2 bytes │ L2 bytes │ remaining bytes │
└──────────┴────────────┴────────────┴──────────────┴──────────────┴──────────────────┘
| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 1 | uint8 | msg_type — always 0x01 for PCM audio |
| 1 | 2 | uint16 LE | sid_length |
| 3 | L1 | UTF-8 | speaker_id |
| 3+L1 | 2 | uint16 LE | sname_length |
| 5+L1 | L2 | UTF-8 | speaker_name |
| 5+L1+L2 | remaining | int16 LE | raw PCM audio samples |
Audio properties: signed 16-bit PCM, little-endian, 48 kHz, mono, no container. Duration = len(pcm)/2 / 48000 seconds.
When MeetStream can't attribute audio to a participant, speaker_name comes through as "NoSpeaker". Filter or label these in your pipeline.
Full decoders for Python, Node.js, Go, and Java are in references/code-patterns-python.md and references/code-patterns-node.md.
Pattern 8: Live Video Streaming (fMP4 over WebSocket)
Supported on Google Meet and Microsoft Teams only — not Zoom.
{
"video_required": true,
"live_video_required": { "websocket_url": "wss://your-server.com/video" }
}
Protocol (all messages from MeetStream to you, except where noted):
| Message type | Direction | Description |
|---|---|---|
video_stream_start (JSON text) | MS → you | Once on connect. Carries codec, audio_codec, container: "fmp4", width, height, framerate, audio_sample_rate, audio_bitrate. |
| binary frames | MS → you | Raw fMP4 (fragmented MP4) bytes. Append in order to build the stream/file. |
video_latency_ping (JSON text) | MS → you | Periodic. Carries seq and sent_at_ms. |
video_latency_pong (JSON text) | you → MS | Echo seq, sent_at_ms, bot_id. Add server_received_at_ms (your wall-clock ms when you handled the ping). |
video_stream_end (JSON text) | MS → you | Sent when stream stops. Carries duration_seconds. Connection closes after. |
Production tips: terminate TLS in front of your app (expose wss://), allow large WS frames, process writes sequentially per stream so chunk order is preserved.
Pattern 9: MIA — AI Agent in a Meeting
MIA (MeetStream Infrastructure Agents) creates a server-configured AI agent that joins meetings via a hosted bridge. Two modes:
pipeline: mix STT + LLM + TTS providers (configurable per layer)realtime: a single realtime model (OpenAI realtime, xAI, Google Gemini) — lower latency
Create the agent config first (in the dashboard at app.meetstream.ai, or via the API):
POST /api/v1/mia
{
"agent_name": "Meeting Assistant",
"mode": "pipeline",
"model": { "provider": "openai", "model": "gpt-4.1", "system_prompt": "..." },
"voice": { "provider": "openai", "voice_id": "nova" },
"transcriber": { "provider": "deepgram", "model": "nova-3", "language": "en" },
"agent": { "response_type": "voice", "first_message": "Hello!" }
}
Returns { message, agent_config_id, agent_config: {...} }.
Then attach it to a bot — pass only agent_config_id:
POST /bots/create_bot
{
"meeting_link": "https://meet.google.com/...",
"bot_name": "Assistant",
"agent_config_id": "<from above>"
}
That's all MIA needs — MeetStream runs the agent on its own hosted bridge. Do not pass socket_connection_url or live_audio_required for MIA; those are only for the bring-your-own-bridge patterns 6–8 and point at your server.
Other MIA operations:
GET /api/v1/mia— list all (no params) or fetch one (?agent_config_id=...)PUT /api/v1/mia— update; body requiresagent_config_id+ fields to changeDELETE /api/v1/mia?agent_config_id=...— delete (query param, not path)
Supported providers: see references/api-reference.md MIA section.
Bot Lifecycle — Live-Verified, Two Distinct Paths
Lifecycle depends on whether you configured a post-call provider or a streaming-only provider. This is the most important architectural distinction in the entire API — and it's not in the official docs.
Path A: Post-call provider (deepgram, assemblyai, sarvam, jigsawstack, meetstream)
create_bot (returns transcript_id)
→ bot.joining (Joining, 200)
→ bot.inmeeting (InMeeting, 200)
→ bot.recording (Recording, 200)
→ participant_events.* (if realtime_endpoints configured)
→ bot.leaving (Leaving, 200)
→ bot.stopped (Stopped|NotAllowed|Denied|Error, 200)
→ manifest.completed (200; manifest_status=Success)
→ audio.processed (200; audio_status=Success)
→ transcription.processed OR transcription.failed ← only path A gets these
→ video.processed (if video_required: true)
→ bot.done (Done, 200|500) ← only path A gets this
→ data_deletion (200) — only after DELETE /bots/{id}/delete or retention expiry
Path B: Streaming-only provider (meetstream_streaming, assemblyai_streaming, deepgram_streaming, jigsawstack_streaming, meeting_captions)
create_bot (returns transcript_id: null)
→ bot.joining (Joining, 200)
→ [bot.error (InMeeting, sc=-)] ← if streaming provider has auth issue
→ bot.inmeeting (InMeeting, 200)
→ bot.recording (Recording, 200)
→ [live chunks stream to live_transcription_required.webhook_url]
→ bot.leaving (Leaving, 200)
→ bot.stopped (Stopped|NotAllowed|Denied|Error, 200)
→ manifest.completed (200)
→ audio.processed (200)
→ [END] ← no transcription.processed, no transcription.failed, no bot.done
Key difference (live-verified): Streaming providers terminate the lifecycle at audio.processed. There is no bot.done, no transcription.processed, no transcription.failed event for streaming-only bots. If your webhook handler waits for bot.done to mark the session complete, it will wait forever on a streaming bot.
Path C: Backup / fallback — /transcribe
Use Path C only as a recovery / fallback. The primary post-call workflow is Path A (configure the post-call provider on
create_botup front). Reach for Path C when:
- You used a streaming-only provider at meeting time and now want a post-call transcript too
- The original
create_botprovider failed (e.g.transcription.failed) and you want to retry with a different provider that has credit- You want to re-transcribe with a higher-quality / different-language provider
If you used a streaming-only provider at meeting time, a post-call transcript does NOT exist automatically — transcript_id stays null, /transcriptions is empty, bot_details.transcript_id is None. The audio recording IS saved (AudioStatus: Success), so you can request transcription whenever you want.
Call POST /bots/{bot_id}/transcribe with a post-call provider any time after bot.stopped:
POST /bots/{bot_id}/transcribe
{
"provider": { "deepgram": { "model": "nova-3", "language": "en" } },
"callback_url": "https://your-server.com/webhook"
}
Live-verified behavior:
- Returns immediately with a new
transcript_idand starts processing in the background - Overwrites
bot_details.transcript_idwith the new id (canonical fetch always returns the latest run) - Adds an entry to
GET /bots/{bot_id}/transcriptionswithstatus: "Processing"→"Success"or"Failed" - Fires exactly one
transcription.processedortranscription.failedevent on thecallback_urlyou pass (single fire-and-forget — NOT a full lifecycle restart) - The webhook payload shape is identical to a Path-A bot's transcription event (same
eventname, sametranscript_status, samestatus_code200/500) - Does NOT fire
bot.doneafter —bot.doneis the Path-A terminal event only - Does NOT include
custom_attributesin the payload (unlike original lifecycle events). Correlate bybot_idinstead.
This is the recommended pattern when you need BOTH live and post-call output, or when your account only has streaming-capable providers configured at bot-creation time but you want to add a paid post-call transcript later.
Notes:
bot.joiningmay fire up to 3 times if server-side join retries kick in.bot.inmeeting,bot.recording,bot.leaving,bot.stopped,bot.doneeach fire at most once per bot.- If the bot fails to join (NotAllowed / Denied), you may skip straight from
bot.joining→bot.stoppedwith nobot.inmeeting,bot.recording, orbot.leaving. bot.doneis the TRUE terminal event, fired after all post-call processing completes (or fails). Use this — notbot.stopped— to mark a session fully done.- Never fetch the transcript before
transcription.processedfires. Always setcallback_url.
Full event reference
| event | bot_status | status_code | When | Extra payload fields |
|---|---|---|---|---|
bot.joining | Joining | 200 | Bot is connecting | — |
bot.error | InMeeting | — (omitted) | Streaming-provider upstream error during meeting (e.g. AssemblyAI "Insufficient funds"). Does NOT abort — bot continues, just no live transcription. Live-verified, no status_code, no custom_attributes, no timestamp. | message (upstream error) |
bot.inmeeting | InMeeting | 200 | Bot joined the meeting | — |
bot.recording | Recording | 200 | Recording started | — |
participant_events.join / .leave | — | — | Participants join/leave (requires recording_config.realtime_endpoints) | Nested: data.data.action, data.data.participant.{id,name,full_name,platform}, data.data.timestamp.{relative,absolute}, data.bot.{id,metadata}. No top-level bot_id or bot_status. |
bot.leaving | Leaving | 200 | Bot is leaving (e.g. removed by host, meeting ended) | — |
bot.stopped | Stopped / NotAllowed / Denied / Error | 200 | Bot exited the meeting | — |
manifest.completed | — | 200 | Platform manifest uploaded | status: "success", manifest_status: "Success", timestamp |
audio.processed | — | 200 | Audio ready to fetch | status: "success", audio_status: "Success", timestamp |
transcription.processed | — | 200 | Transcript ready — POST-CALL PROVIDERS ONLY | transcript_status: "Success", timestamp |
transcription.failed | — | 500 | Transcript failed — POST-CALL PROVIDERS ONLY | status: "error", transcript_status: "Failed", message (error detail), timestamp |
video.processed | — | 200 | Video ready to fetch (video_required: true) | video_status: "Success", timestamp |
bot.done | Done | 200 (or 500 if processing failed) | All post-call processing complete — terminal event, PATH A ONLY | timestamp, message (error detail if status_code=500) |
data_deletion | — | 200 | Data deleted (manual /delete or retention) | status: "success", deleted_objects: <int>, timestamp |
Streaming-only bots (Path B) never fire
transcription.processed,transcription.failed, orbot.done. Their lifecycle ends ataudio.processed.bot.erroris specific to streaming providers when their upstream auth fails. If your handler waits forbot.doneto mark sessions complete, it will hang forever on streaming bots — useaudio.processedas the terminal signal for Path B.
status_code is NOT always 200
Failure events carry status_code: 500. The only failure event that's been live-observed so far is transcription.failed. bot.done also carries status_code: 500 when its preceding processing event errored. Always branch on event first; use status_code only as a quick error indicator.
Lifecycle internal states (visible via bot_details.StatusTimeline)
StatusTimeline on /bots/{bot_id}/detail shows every stage the bot passed through. Per live observation, the full set of stages is:
Scheduled, Joining, WaitingRoom, InMeeting, Recording, Leaving, Stopped, BotNotAllowed, BotRejected, Error, AudioProcessing, VideoProcessing, TranscriptionReady, Done, MediaExpired
Each entry is {message, status, timestamp}. status: true means that stage was reached; false means it wasn't (e.g. BotRejected.status: false if the bot wasn't rejected).
automatic_leave Timeouts — Recommended Defaults
{
"automatic_leave": {
"waiting_room_timeout": 600,
"everyone_left_timeout": 600,
"voice_inactivity_timeout": 600,
"in_call_recording_timeout": 14400,
"recording_permission_denied_timeout": 300
}
}
All values in seconds. Use these defaults unless you have a specific reason to change them — they prevent the bot from dropping out of long real meetings.
waiting_room_timeout— wait in waiting room before leaving — 600s (10 min)everyone_left_timeout— stay after everyone else leaves — 600s (10 min)voice_inactivity_timeout— wait if no audio detected (someone could be presenting silently) — 600s (10 min)in_call_recording_timeout— max recording duration — 14400s (4 hours), the canonical default for full-length meetingsrecording_permission_denied_timeout— wait if recording permission denied (Zoom-only) — 300s (5 min) — this is the max the API allows (range is 60–300)
Live-tested constraints (not in OpenAPI spec):
in_call_recording_timeoutminimum is 600 seconds — the live API returnsHTTP 400: "in_call_recording_timeout must be at least 600 seconds"below that.recording_permission_denied_timeoutaccepted range is 60–300 seconds (live-verified). Below 60 returns HTTP 400; above 300 returnsHTTP 400: "recording_permission_denied_timeout must not exceed 300 seconds". Use 300 (the max) for maximum patience.
Why 14400 for in_call_recording_timeout specifically: real meetings often run longer than 30 minutes. Defaulting to anything less risks the bot disconnecting while people are still talking. 4 hours is the canonical safe value for full-length sales / interview / workshop sessions.
Without these defaults, a stuck bot can sit in a meeting indefinitely.
bot_image_url
Custom profile picture must be a publicly accessible URL — MeetStream fetches it externally. The OpenAPI schema describes it as a URL string. Base64 / data URIs are not supported by the docs guide; the image must be reachable without authentication.
{ "bot_image_url": "https://your-server.com/avatar.png" }
custom_attributes
The OpenAPI schema allows additionalProperties: { description: Any type }. Arbitrary key-value metadata; values are echoed back in webhook payloads.
Observed behavior: in practice the server has been seen to reject non-string values. The OpenAPI doesn't enforce string-only, but stringifying values defensively is safer:
{ "custom_attributes": { "deal_id": "12345", "active": "true", "score": "0.87" } }
Platform Notes (per-feature, not just per-bot)
| Feature | Google Meet | Zoom | Microsoft Teams |
|---|---|---|---|
Basic bot (create_bot) | ✓ | ✓ (setup required) | ✓ |
| Recording + post-call transcript | ✓ | ✓ | ✓ |
| Live transcription webhook | ✓ | ✓ | ✓ |
Live audio (live_audio_required) | ✓ | ✓ | ✓ |
Live video (live_video_required) | ✓ | ✗ | ✓ |
| Per-participant audio streams | ✓ | ✓ | ✗ |
| Per-participant video streams | ✓ | ✓ | ✓ |
| MIA agents | ✓ | ✓ | ✓ |
interrupt WS command (clears audio queue) | ✓ | ✗ (accepted, no-op) | ✗ (accepted, no-op) |
Native captions (meeting_captions) | ✓ | ✗ | ✓ |
Zoom setup
Register a Zoom app + add credentials to MeetStream dashboard. Guide: https://docs.meetstream.ai/guides/app-integrations/zoom-marketplace-app-setup
Zoom dev mode restricts bots to meetings hosted by the app owner's account. For external meetings, follow the production submission guide: https://docs.meetstream.ai/guides/app-integrations/zoom-app-production-submission
For Zoom OBF (On-Behalf-Of), pass "zoom": { "use_zoom_obf": true } on create_bot.
The Zoom OAuth callback URL to register in your Zoom app is: https://api.meetstream.ai/api/v1/admin/zoom/oauth/callback
Google Signed-In Bots
To have the bot join Google Meet using a Google identity (for paywalled / signed-in-only meetings), configure SSO in your Google Workspace admin and register domain/login certificates via:
POST /google-login-domains— register a Google Workspace domainGET /google-login-domains— list domainsGET /google-login-domains/{domain}— fetch one (path param is the workspace domain string)PUT /google-login-domains/{domain}/DELETE /google-login-domains/{domain}— update / deletePOST /google-logins/GET /google-logins/PUT/DELETE /google-logins/{id}— manage logins
Then, on create_bot, pass the google_meet field (documented in the Google Signed-In Bots guide):
{
"google_meet": {
"login_required": true,
"google_login_domain": "your-domain.com"
}
}
Note:
google_meetis documented in the prose guide but isn't in the OpenAPICreateBotRequestschema. The Google Signed-In Bots guide is the canonical reference: https://docs.meetstream.ai/guides/app-integrations/google-signed-in-bots
Transcription Providers
Specified under recording_config.transcript.provider. Use exactly one provider per bot.
| Provider key | Mode | Notes |
|---|---|---|
meetstream | Post-call | MeetStream's in-house post-call engine. OpenAPI marks language and translate as required. |
deepgram | Post-call | nova-3 is the only model enum. Requires Deepgram key in account. language defaults to "en" (free string per spec). |
assemblyai | Post-call | Speaker diarization, redaction, chapters. Requires AssemblyAI key in account. 9 fields marked required per OpenAPI. |
sarvam | Post-call | Indic languages, e.g. model: "saaras:v3", language_code: "en-IN". Requires Sarvam key. |
jigsawstack | Post-call | Auto language detect, optional translation. Requires JigsawStack key. |
meeting_captions | Live, native | Free — uses Meet / Teams native captions. No external key needed. transcript_id is null — fetch captions via bot_details.caption_file on /detail. |
meetstream_streaming | Live | MeetStream in-house streaming — works without external keys. Live-verified. Use this for live transcription on stock accounts. Empty config {} accepted. No post-call transcript fetch (returns HTTP 202 forever). |
deepgram_streaming | Live | Real-time. All ~12 fields marked required per OpenAPI. Needs Deepgram key. |
assemblyai_streaming | Live | Real-time English. All ~10 fields marked required per OpenAPI. Needs AssemblyAI key. |
jigsawstack_streaming | Live | Live-verified existence (referenced in API error message); schema not yet documented in OpenAPI. |
Account configuration matters. Provider keys must be added to your MeetStream account dashboard before use. If a key is missing or invalid, the
transcription.failedevent fires withstatus_code: 500and amessagedescribing the upstream auth failure (e.g."Deepgram API error: 401"or"Unauthorized Connection: Insufficient funds"). Test your account's provider configuration before relying on a post-call provider in production.
Required-vs-default: the OpenAPI marks many sub-fields as
requiredeven when they have natural defaults. Pass full provider configs when in doubt — seereferences/api-reference.mdfor the full schema of each provider.
Webhook Handler Requirements
- Must use HTTPS (use ngrok or cloudflared for local dev — see https://docs.meetstream.ai/guides/webhooks/local-webhook-server)
- Must respond
2xxquickly to acknowledge receipt - No automatic retries. If your endpoint returns non-2xx, the webhook is not retried. Build your handler to never fail (catch errors, queue work, return 200).
bot.joiningmay fire up to 3 times if join retries are configured.bot.inmeeting,bot.stopped,audio.processed,transcription.processed,video.processed,data_deletionare each sent at most once.- Idempotency: the docs recommend
{bot_id, event, timestamp}as a dedupe key, but lifecycle events (bot.joining,bot.inmeeting,bot.recording,bot.leaving,bot.stopped) do NOT carry atimestampfield (live-verified). For those, fall back to{bot_id, event, message}or accept thatbot.joiningmay legitimately fire up to 3× and skip dedupe on lifecycle events. Only post-call events (*.processed,*.failed,manifest.completed,bot.done,data_deletion) have a stabletimestampfor dedup.
Webhook payload shape
Lifecycle events (bot.joining/bot.inmeeting/bot.recording/bot.leaving/bot.stopped) — minimal envelope, no timestamp:
{
"event": "bot.joining",
"bot_id": "dd451299-1471-49ae-b407-c91541242748",
"bot_status": "Joining",
"message": "Bot is joining the meeting",
"status_code": 200,
"custom_attributes": { "your_keys": "echoed back" }
}
Post-call events (manifest.completed, audio.processed, transcription.processed, transcription.failed, video.processed, bot.done, data_deletion) — include timestamp and may include status: "success"|"error":
{
"event": "transcription.failed",
"bot_id": "...",
"status": "error",
"transcript_status": "Failed",
"message": "Transcript processing error: Deepgram API error: 401",
"status_code": 500,
"custom_attributes": {...},
"timestamp": "2026-05-26T10:11:25.781273+00:00"
}
Important — live-verified, contradicting earlier skill claims:
timestampis NOT on every event. Only post-call events have it.status_codeis NOT always 200. Failure events use 500.participant_events.*payload has a different structure —bot_idis nested underdata.bot.id, no top-levelbot_status.- Branch on
eventfirst.
Event types (see live-verified table in Bot Lifecycle)
12+ events: bot.joining, bot.inmeeting, bot.recording, participant_events.*, bot.leaving, bot.stopped, manifest.completed, audio.processed, transcription.processed, transcription.failed, video.processed, bot.done, data_deletion. Bold = added based on live testing.
bot_status values
| bot_status | Event(s) | Meaning |
|---|---|---|
Joining | bot.joining | Bot is connecting |
InMeeting | bot.inmeeting | Bot is in the meeting |
Recording | bot.recording | Recording active |
Leaving | bot.leaving | Bot is leaving in progress |
Stopped | bot.stopped | Normal exit (meeting ended, removed via API, voice timeout, host removed) |
NotAllowed | bot.stopped | Could not join (waiting-room / lobby timeout) |
Denied | bot.stopped | Host denied join or recording permission |
Error | bot.stopped | Unexpected error during lifecycle |
Done | bot.done | All processing complete — terminal |
*.failed events DO exist (skill previously claimed they didn't)
Live-verified: transcription.failed is a real event with status_code: 500, transcript_status: "Failed", and message carrying the upstream error (e.g. provider auth failure). Handle these in your router:
elif event_type == "transcription.failed":
log_error(f"Transcript failed for bot {bot_id}: {payload.get('message')}")
elif event_type == "bot.done" and payload.get("status_code") == 500:
log_error(f"Bot finished with error: {payload.get('message')}")
Symmetric audio.failed and video.failed events have NOT been observed in live testing. Don't assume they exist — handle failures by checking bot_details.AudioStatus and bot_details.TranscriptStatus, and by watching for bot.done with status_code: 500 (which carries the upstream error in message).
Webhook signature verification (optional)
If a webhook secret is configured in the MeetStream dashboard, requests include:
X-MeetStream-Signature: sha256=<hex_digest>—HMAC-SHA256(secret, raw_body)X-MeetStream-Timestamp: <iso8601>
Verify by computing HMAC-SHA256(secret, raw_request_body), comparing against the signature (strip the sha256= prefix), and optionally checking the timestamp is within an acceptable replay window.
Data Available After Meeting
After the relevant *.processed event fires:
| Data | Endpoint | Notes |
|---|---|---|
| Transcript (formatted) | GET /transcript/{transcript_id}/get_transcript | Response is a top-level array of segments |
| Raw transcript | GET /transcript/{transcript_id}/get_transcript?raw=true | Raw provider output |
| List transcripts for a bot | GET /bots/{bot_id}/transcriptions | Returns {bot_id, transcriptions: [{transcript_id, provider, status, created_at, config, download_urls}]} |
| Trigger a new transcription run | POST /bots/{bot_id}/transcribe | Body: {provider: {<key>: {...}}, callback_url?} |
| Speaker timeline | GET /bots/{bot_id}/get_speaker_timeline | Returns chunks with byte offsets into the audio file (not time) |
| In-meeting chat | GET /bots/{bot_id}/get_chats | |
| Audio file | GET /bots/{bot_id}/get_audio | Pre-signed S3 URL, valid 1 hour |
| Video file | GET /bots/{bot_id}/get_video | Pre-signed S3 URL, valid 10 minutes |
| Per-participant audio streams | GET /bots/{bot_id}/get_audio_streams | Per-segment URLs valid 10 minutes; returns 202 while bot is still in meeting |
| Per-participant recording streams | GET /bots/{bot_id}/get_recording_streams | Per-segment URLs valid 10 minutes; includes both participants[] and screenshares[] |
| Participant list | GET /bots/{bot_id}/get_participants | Returns a top-level array of {deviceId, displayName, fullName, profilePicture, status, humanized_status, streamIds[], lastUpdated, parentDeviceId?} |
| Session metadata | GET /bots/{bot_id}/detail | Returns bot_details with (live-verified): BotID, BotImageURL, BotMessage, BotProfile, BotUsername, CreatedAt, Duration, EndTime, LastUpdatedAt, ManifestStatus, MediaS3Bucket, MeetingLink, NativeSTT, OfferingType, Platform, PlatformCreatedAt, PlatformStatusCreatedAt, RequestPayload, StartTime, Status, StatusCreatedAt, StatusTimeline (per-stage {message, status, timestamp} map), AudioStatus, TranscriptStatus, UserID, caption_file (S3 link for native captions), participant_events, custom_attributes, and transcript_id (top-level — populated when a post-call provider is set, not enumerated in OpenAPI but returned live). |
| Screenshots | GET /bots/{bot_id}/get_screenshots | |
| Summary (if generated) | GET /bots/{bot_id}/summary | (Path is /summary, not /get_summary.) |
Bot management:
- Status:
GET /bots/{bot_id}/status - List bots:
GET /bots— returns{bots: [...], hasNextPage: bool, nextCursor: string|null}(note camelCase pagination keys) - Remove from active meeting:
GET /bots/{bot_id}/remove_bot(it's GET in the OpenAPI; the Quickstart docs page showscurl -X POSTbut that contradicts the spec) - Delete data:
DELETE /bots/{bot_id}/delete→ firesdata_deletionwebhook
Per-participant audio/video — concurrency & platform limits
- Per-participant audio captures up to 16 concurrent speaker streams. Supported on Google Meet + Zoom only, NOT Teams. Files: WebM container, Opus codec, 48 kbps, 48 kHz, mono.
- Per-participant video captures up to 6 concurrent webcam streams. Supported on all 3 platforms. Files: WebM container, VP8 codec, 15 FPS, video-only (audio fetched separately).
- Both flags (
audio_separate_streams,video_separate_streams) can be set at the top level ofcreate_botOR nested insiderecording_config.
Data Retention
Default retention is 24 hours. Override on create_bot:
{ "recording_config": { "retention": { "type": "timed", "hours": 168 } } }
⚠️ DELETE /bots/{bot_id}/delete — DESTRUCTIVE
Do not call this casually. It permanently deletes the recording data for a bot:
- The audio recording (MP3/WAV)
- The video recording (MP4)
- Per-participant audio/video streams
- The transcript
- Any associated S3 objects
There is no recovery. Once deleted, no API call (including the transcript endpoint) will return the data — every subsequent fetch will 404.
Live test response confirms scope: {"message": "Successfully deleted 7 objects for bot ...", "bot_id": "...", "deleted_objects": 7} — that "7 objects" includes audio, video, transcript, and metadata blobs.
Rules:
- Never call
/deleteautomatically in webhook handlers, agents, or polling loops. If you wire this into automated code, you will eventually wipe a session you needed. - Never call
/deleteto "clean up" a test bot until you've verified you don't need its outputs. The cleanup paths in the code patterns below use/deleteonly because they're explicitly test scaffolding. - Prefer retention-based expiry. Set
recording_config.retentionto the shortest acceptable window for your use case (default 24 hours) and let MeetStream's scheduler clean up. - Confirm with the user before running
/deleteinteractively. Treat it likerm -rf— if the user didn't explicitly ask to delete, don't. - The
data_deletionwebhook fires after deletion — receiving it means data is already gone, not "about to be deleted."
# DESTRUCTIVE — only with explicit confirmation
def delete_bot_data(bot_id: str, confirmed: bool = False) -> dict:
"""Deletes audio, video, transcript permanently. Requires confirmed=True."""
if not confirmed:
raise ValueError("Refusing destructive delete without explicit confirmed=True flag")
resp = requests.delete(f"{BASE_URL}/bots/{bot_id}/delete", headers=HEADERS)
resp.raise_for_status()
return resp.json() # {message, bot_id, deleted_objects}
Common Mistakes
- Looking for
transcript_idin the webhook — it's not there. The webhook only signals timing. Get thetranscript_idfrom thecreate_botresponse,GET /bots/{bot_id}/detail(bot_details.transcript_id), orGET /bots/{bot_id}/transcriptions. - Iterating
transcript.transcript[]withseg.text— the response is a top-level array, and the per-segment text field istranscript, nottext. send_imagebody usingimage_url— the field isimg_url.- Fetching transcript before
transcription.processed— always wait for the webhook. - HTTP callback URL — must be HTTPS; use ngrok / cloudflared for local dev.
- Wrong calendar field names — Calendar create requires
google_refresh_token/google_client_id/google_client_secret(with thegoogle_prefix). - Using
audio_requiredoncreate_bot— not in the OpenAPI schema. Audio is captured by default. (It DOES exist for calendar-scheduledbot_config.) - Skipping
bot_name— it's required, not optional. - Assuming
video_requireddefaults to false — it defaults totrue. Set it tofalsefor transcript-only workflows or you'll burn storage. - Assuming
status_codeis always 200 — it's 200 for success events, 500 for failure events (e.g.transcription.failed,bot.donewhen processing errored). Branch oneventfirst. - Expecting webhook retries — there are none. Always return 2xx, queue work asynchronously. For dedup, note that lifecycle events lack
timestampso the docs-recommended{bot_id, event, timestamp}key collapses to{bot_id, event, undefined}for those — fall back to{bot_id, event, message}or skip dedup on lifecycle events. - Using
websocket_urlfor live transcription —live_transcription_requiredaccepts onlywebhook_url(HTTPS POST). Thewebsocket_urlfield is forlive_audio_required,live_video_required, andsocket_connection_url. - Sending WAV blobs over
sendaudio— bot expects raw PCM16 LE @ 48000 Hz mono, base64-encoded. No WAV header. Resample if your source isn't 48 kHz. /calendar/toggle-recurring/{event_id}path — actual path isPOST /calendar/toggle-recurrencewith body{event_id, recurring_enabled}. No path param.- MIA endpoint paths — they are
/api/v1/mia(singular) for CRUD; DELETE uses?agent_config_id=...query param, not a path id. - Live video on Zoom — not supported. Google Meet and Teams only.
in_call_recording_timeoutunder 600 — live API rejects with HTTP 400. Minimum 600 seconds.- Expecting
create_botto return HTTP 200 — it returns HTTP 201 Created (per OpenAPI and live-verified).statusfield is typically"Active". - Expecting a post-call transcript from a streaming-only provider — streaming providers (
meetstream_streaming,assemblyai_streaming,deepgram_streaming,jigsawstack_streaming,meeting_captions) returntranscript_id: nulland never populatebot_details.transcript_idor/transcriptions. The audio is saved but no transcript exists until you manually callPOST /bots/{bot_id}/transcribe. - Waiting for
bot.doneon a streaming-only bot — that event never fires for Path B. The terminal event isaudio.processed. Handlers that block onbot.donewill hang. - Treating
bot.erroras a fatal event — it's only a streaming-provider warning (upstream auth issue). The bot continues recording; only live transcription is impacted. - Waiting for
bot.doneafter/transcribe—/transcribeis fire-and-forget. You get exactly onetranscription.processed/transcription.failed, nobot.donefollow-up. - Relying on
custom_attributesin/transcribe-triggered webhooks — that field is missing from/transcribeevents (present on original lifecycle events). Correlate bybot_idinstead. - Polling
/transcript/{tid}/get_transcriptwithout a bound — on failure it returns HTTP 202 forever. Always checkbot_details.TranscriptStatus(authoritative:"Success"/"Failed"/None) or watch for thetranscription.failedwebhook to break out.
Code Reference Files
Read these when building:
references/code-patterns-node.md— complete Node.js/TypeScript implementationsreferences/code-patterns-python.md— complete Python implementations (including live audio decoder, MIA setup, fMP4 video receiver)references/api-reference.md— full endpoint map with params, request bodies, response shapes
When to consult docs directly
For specific endpoint parameters, response schemas, or edge cases not covered here:
- MCP server:
https://docs.meetstream.ai/_mcp/server - OpenAPI:
https://docs.meetstream.ai/openapi.json - Any docs page as clean markdown: append
.mdto its URL, e.g.https://docs.meetstream.ai/api-reference/api-endpoints/bot-endpoints/create-bot.md - Full single-file dump:
https://docs.meetstream.ai/llms-full.txt
Never invent endpoints or fields. If the docs don't show it, don't put it in code.