agentsclimarketplace

Meetstream

Skill meetstream-ai/claude-plugin/skills/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

Install
npx -y skills add meetstream-ai/claude-plugin --skill meetstream

Assembled 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 from https://docs.meetstream.ai/<path>.md (append .md to any docs page URL for clean markdown) or via the MCP server at https://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:

  1. Invoke the superpowers:writing-plans skill first
  2. Create the project folder if it doesn't exist
  3. Read references/code-patterns-node.md or references/code-patterns-python.md for the relevant patterns
  4. Write the full plan to docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md
  5. 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:

  1. Signup at https://app.meetstream.ai (free tier, no credit card)
  2. API key creation at https://app.meetstream.ai/api-keys
  3. Environment variable setup (export MEETSTREAM_API_KEY=ms_xxxxx + .zshrc/.bashrc/.env)
  4. Validation ping
  5. Optional handoff to verify-account to 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:

  1. API key — if not provided, ask. Link: https://app.meetstream.ai/api-keys
  2. Platform — Google Meet, Teams, Zoom, or all? (Zoom needs extra setup; see Platform Notes)
  3. Language — Python or Node.js/TypeScript?
  4. 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 URL
  • bot_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_required DOES exist for calendar-scheduled bots — inside bot_config on POST /calendar/schedule/{event_id}, the auto-schedule default_bot_config, etc. The "doesn't exist" rule only applies to create_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

NeedWhat to configureLifecycle terminal eventWhere the transcript lives
Post-call transcript (notetaker, recordings, summaries) — the most common use caserecording_config.transcript.provider set to a post-call provider (deepgram / assemblyai / sarvam / jigsawstack / meetstream) + a callback_url for webhooksbot.doneGET /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):

  1. create_bot with recording_config.transcript.provider: {deepgram: {...}} (or assemblyai, etc.) and callback_url
  2. Wait for the transcription.processed webhook
  3. GET /bots/{bot_id}/detail → read bot_details.transcript_id
  4. GET /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.failed because 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_required defaults to true which 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 on create_bot (audio is always captured). It DOES exist in calendar-scheduled bot_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_timeout minimum 600s (API HTTP 400 below)
  • recording_permission_denied_timeout range 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:

  1. API-triggered notetaker — POST /meetings/start → join → summarize → email all attendees + trigger user via Resend (use notetaker skill)
  2. Real-time sales coach — meeting link → live transcription → LLM detection → WebSocket push to browser (use sales-coach skill)
  3. Calendar auto-recording — Google Calendar OAuth → auto-schedule → summary delivery (use calendar-automation + notetaker skills)
  4. CRM auto-enricher (HubSpot/Salesforce) — post-call → extract action items + signals → upsert CRM activity (use notetaker skill + CRM SDK)
  5. Interview recorder for HR/recruiting — per-participant audio + structured interview report (use notetaker skill with audio_separate_streams: true)
  6. Multi-tenant notetaker SaaS — per-user OAuth, per-user calendars, billing-ready (use calendar-automation + notetaker skills with tenant-scoped routing)
  7. Compliance recorder — 90-day retention + per-participant audio + audit log + KMS-encrypted transcripts (use notetaker skill with retention.hours: 2160 + audit logging)
  8. AI Meeting Assistant (MIA) — voice-interactive bot that joins and participates (use MIA pattern in meetstream core skill)
  9. Live captions broadcaster — accessibility-focused live transcript via SSE (use sales-coach skill structure but render captions instead of cards)
  10. Customer call analyzer + insights dashboard — structured extraction + week-over-week analytics (use notetaker skill + 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:

  1. Wait for the transcription.processed webhook (signals readiness — but does not contain transcript_id)
  2. Call GET /bots/{bot_id}/detail → read bot_details.transcript_id
  3. 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.processed webhook never fires (e.g. provider auth failure → transcription.failed), bot_details.transcript_id is 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):

SourceValue on transcript failure
bot_details.TranscriptStatus"Failed" ← authoritative
/transcriptions[].status"Not started" (stale)
/transcript/{tid}/get_transcript HTTP202 forever

Always check bot_details.TranscriptStatus before retrying, or watch for the transcription.failed webhook 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:

SourceWhen to use
bot_details.transcript_id on GET /bots/{bot_id}/detailCanonical / default path. Stateless — bot_id from webhook is all you need.
BotResponse.transcript_id in create_bot responseAvailable immediately on bot creation if you want to log it; not required.
GET /bots/{bot_id}/transcriptionsOnly 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 responseWhen 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_url REQUIRES a streaming provider in recording_config.transcript.provider. The live API returns HTTP 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_streaming
  • meetstream_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:

CommandPurpose
sendaudioPlay raw PCM16 LE audio (48 kHz mono, base64-encoded) through the bot's mic
sendmsgPost a chat message (requires BOTH message AND msg fields with the same value, for cross-platform compat)
sendchatChat with role tagging + streaming support (role, text, is_final)
interruptStop bot audio playback ({action: "clear_audio_queue"}) — Google Meet only; Zoom/Teams accept but don't clear
sendimg / sendimg_urlSet 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_image field is img_url (not image_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):

MethodPathPurpose
POST/calendar/create_calendarConnect a Google Calendar
GET/calendar/calendarsList connected calendars (live from Google)
GET/calendar/eventsSync + list events with pagination + bot info
GET/calendar/get_eventsList 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_botsList 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/enableEnable auto-scheduling (body: default_bot_config)
POST/calendar/auto-schedule/disableDisable auto-scheduling
GET/calendar/auto-schedule/settingsRead current auto-schedule settings
POST/calendar/auto-rescheduleTrigger reschedule for next recurring occurrence
POST/calendar/toggle-recurrenceEnable/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 │
└──────────┴────────────┴────────────┴──────────────┴──────────────┴──────────────────┘
OffsetSizeTypeField
01uint8msg_type — always 0x01 for PCM audio
12uint16 LEsid_length
3L1UTF-8speaker_id
3+L12uint16 LEsname_length
5+L1L2UTF-8speaker_name
5+L1+L2remainingint16 LEraw 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 typeDirectionDescription
video_stream_start (JSON text)MS → youOnce on connect. Carries codec, audio_codec, container: "fmp4", width, height, framerate, audio_sample_rate, audio_bitrate.
binary framesMS → youRaw fMP4 (fragmented MP4) bytes. Append in order to build the stream/file.
video_latency_ping (JSON text)MS → youPeriodic. Carries seq and sent_at_ms.
video_latency_pong (JSON text)you → MSEcho 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 → youSent 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 requires agent_config_id + fields to change
  • DELETE /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_bot up 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_bot provider 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_id and starts processing in the background
  • Overwrites bot_details.transcript_id with the new id (canonical fetch always returns the latest run)
  • Adds an entry to GET /bots/{bot_id}/transcriptions with status: "Processing" → "Success" or "Failed"
  • Fires exactly one transcription.processed or transcription.failed event on the callback_url you 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 event name, same transcript_status, same status_code 200/500)
  • Does NOT fire bot.done after — bot.done is the Path-A terminal event only
  • Does NOT include custom_attributes in the payload (unlike original lifecycle events). Correlate by bot_id instead.

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.joining may fire up to 3 times if server-side join retries kick in.
  • bot.inmeeting, bot.recording, bot.leaving, bot.stopped, bot.done each fire at most once per bot.
  • If the bot fails to join (NotAllowed / Denied), you may skip straight from bot.joining → bot.stopped with no bot.inmeeting, bot.recording, or bot.leaving.
  • bot.done is the TRUE terminal event, fired after all post-call processing completes (or fails). Use this — not bot.stopped — to mark a session fully done.
  • Never fetch the transcript before transcription.processed fires. Always set callback_url.

Full event reference

eventbot_statusstatus_codeWhenExtra payload fields
bot.joiningJoining200Bot is connecting—
bot.errorInMeeting— (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.inmeetingInMeeting200Bot joined the meeting—
bot.recordingRecording200Recording 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.leavingLeaving200Bot is leaving (e.g. removed by host, meeting ended)—
bot.stoppedStopped / NotAllowed / Denied / Error200Bot exited the meeting—
manifest.completed—200Platform manifest uploadedstatus: "success", manifest_status: "Success", timestamp
audio.processed—200Audio ready to fetchstatus: "success", audio_status: "Success", timestamp
transcription.processed—200Transcript ready — POST-CALL PROVIDERS ONLYtranscript_status: "Success", timestamp
transcription.failed—500Transcript failed — POST-CALL PROVIDERS ONLYstatus: "error", transcript_status: "Failed", message (error detail), timestamp
video.processed—200Video ready to fetch (video_required: true)video_status: "Success", timestamp
bot.doneDone200 (or 500 if processing failed)All post-call processing complete — terminal event, PATH A ONLYtimestamp, message (error detail if status_code=500)
data_deletion—200Data deleted (manual /delete or retention)status: "success", deleted_objects: <int>, timestamp

Streaming-only bots (Path B) never fire transcription.processed, transcription.failed, or bot.done. Their lifecycle ends at audio.processed. bot.error is specific to streaming providers when their upstream auth fails. If your handler waits for bot.done to mark sessions complete, it will hang forever on streaming bots — use audio.processed as 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 meetings
  • recording_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_timeout minimum is 600 seconds — the live API returns HTTP 400: "in_call_recording_timeout must be at least 600 seconds" below that.
  • recording_permission_denied_timeout accepted range is 60–300 seconds (live-verified). Below 60 returns HTTP 400; above 300 returns HTTP 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)

FeatureGoogle MeetZoomMicrosoft 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 domain
  • GET /google-login-domains — list domains
  • GET /google-login-domains/{domain} — fetch one (path param is the workspace domain string)
  • PUT /google-login-domains/{domain} / DELETE /google-login-domains/{domain} — update / delete
  • POST /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_meet is documented in the prose guide but isn't in the OpenAPI CreateBotRequest schema. 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 keyModeNotes
meetstreamPost-callMeetStream's in-house post-call engine. OpenAPI marks language and translate as required.
deepgramPost-callnova-3 is the only model enum. Requires Deepgram key in account. language defaults to "en" (free string per spec).
assemblyaiPost-callSpeaker diarization, redaction, chapters. Requires AssemblyAI key in account. 9 fields marked required per OpenAPI.
sarvamPost-callIndic languages, e.g. model: "saaras:v3", language_code: "en-IN". Requires Sarvam key.
jigsawstackPost-callAuto language detect, optional translation. Requires JigsawStack key.
meeting_captionsLive, nativeFree — uses Meet / Teams native captions. No external key needed. transcript_id is null — fetch captions via bot_details.caption_file on /detail.
meetstream_streamingLiveMeetStream 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_streamingLiveReal-time. All ~12 fields marked required per OpenAPI. Needs Deepgram key.
assemblyai_streamingLiveReal-time English. All ~10 fields marked required per OpenAPI. Needs AssemblyAI key.
jigsawstack_streamingLiveLive-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.failed event fires with status_code: 500 and a message describing 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 required even when they have natural defaults. Pass full provider configs when in doubt — see references/api-reference.md for 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 2xx quickly 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.joining may fire up to 3 times if join retries are configured. bot.inmeeting, bot.stopped, audio.processed, transcription.processed, video.processed, data_deletion are 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 a timestamp field (live-verified). For those, fall back to {bot_id, event, message} or accept that bot.joining may 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 stable timestamp for 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:

  • timestamp is NOT on every event. Only post-call events have it.
  • status_code is NOT always 200. Failure events use 500.
  • participant_events.* payload has a different structure — bot_id is nested under data.bot.id, no top-level bot_status.
  • Branch on event first.

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_statusEvent(s)Meaning
Joiningbot.joiningBot is connecting
InMeetingbot.inmeetingBot is in the meeting
Recordingbot.recordingRecording active
Leavingbot.leavingBot is leaving in progress
Stoppedbot.stoppedNormal exit (meeting ended, removed via API, voice timeout, host removed)
NotAllowedbot.stoppedCould not join (waiting-room / lobby timeout)
Deniedbot.stoppedHost denied join or recording permission
Errorbot.stoppedUnexpected error during lifecycle
Donebot.doneAll 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:

DataEndpointNotes
Transcript (formatted)GET /transcript/{transcript_id}/get_transcriptResponse is a top-level array of segments
Raw transcriptGET /transcript/{transcript_id}/get_transcript?raw=trueRaw provider output
List transcripts for a botGET /bots/{bot_id}/transcriptionsReturns {bot_id, transcriptions: [{transcript_id, provider, status, created_at, config, download_urls}]}
Trigger a new transcription runPOST /bots/{bot_id}/transcribeBody: {provider: {<key>: {...}}, callback_url?}
Speaker timelineGET /bots/{bot_id}/get_speaker_timelineReturns chunks with byte offsets into the audio file (not time)
In-meeting chatGET /bots/{bot_id}/get_chats
Audio fileGET /bots/{bot_id}/get_audioPre-signed S3 URL, valid 1 hour
Video fileGET /bots/{bot_id}/get_videoPre-signed S3 URL, valid 10 minutes
Per-participant audio streamsGET /bots/{bot_id}/get_audio_streamsPer-segment URLs valid 10 minutes; returns 202 while bot is still in meeting
Per-participant recording streamsGET /bots/{bot_id}/get_recording_streamsPer-segment URLs valid 10 minutes; includes both participants[] and screenshares[]
Participant listGET /bots/{bot_id}/get_participantsReturns a top-level array of {deviceId, displayName, fullName, profilePicture, status, humanized_status, streamIds[], lastUpdated, parentDeviceId?}
Session metadataGET /bots/{bot_id}/detailReturns 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).
ScreenshotsGET /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 shows curl -X POST but that contradicts the spec)
  • Delete data: DELETE /bots/{bot_id}/delete → fires data_deletion webhook

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 of create_bot OR nested inside recording_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:

  1. Never call /delete automatically in webhook handlers, agents, or polling loops. If you wire this into automated code, you will eventually wipe a session you needed.
  2. Never call /delete to "clean up" a test bot until you've verified you don't need its outputs. The cleanup paths in the code patterns below use /delete only because they're explicitly test scaffolding.
  3. Prefer retention-based expiry. Set recording_config.retention to the shortest acceptable window for your use case (default 24 hours) and let MeetStream's scheduler clean up.
  4. Confirm with the user before running /delete interactively. Treat it like rm -rf — if the user didn't explicitly ask to delete, don't.
  5. The data_deletion webhook 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

  1. Looking for transcript_id in the webhook — it's not there. The webhook only signals timing. Get the transcript_id from the create_bot response, GET /bots/{bot_id}/detail (bot_details.transcript_id), or GET /bots/{bot_id}/transcriptions.
  2. Iterating transcript.transcript[] with seg.text — the response is a top-level array, and the per-segment text field is transcript, not text.
  3. send_image body using image_url — the field is img_url.
  4. Fetching transcript before transcription.processed — always wait for the webhook.
  5. HTTP callback URL — must be HTTPS; use ngrok / cloudflared for local dev.
  6. Wrong calendar field names — Calendar create requires google_refresh_token / google_client_id / google_client_secret (with the google_ prefix).
  7. Using audio_required on create_bot — not in the OpenAPI schema. Audio is captured by default. (It DOES exist for calendar-scheduled bot_config.)
  8. Skipping bot_name — it's required, not optional.
  9. Assuming video_required defaults to false — it defaults to true. Set it to false for transcript-only workflows or you'll burn storage.
  10. Assuming status_code is always 200 — it's 200 for success events, 500 for failure events (e.g. transcription.failed, bot.done when processing errored). Branch on event first.
  11. Expecting webhook retries — there are none. Always return 2xx, queue work asynchronously. For dedup, note that lifecycle events lack timestamp so 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.
  12. Using websocket_url for live transcription — live_transcription_required accepts only webhook_url (HTTPS POST). The websocket_url field is for live_audio_required, live_video_required, and socket_connection_url.
  13. 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.
  14. /calendar/toggle-recurring/{event_id} path — actual path is POST /calendar/toggle-recurrence with body {event_id, recurring_enabled}. No path param.
  15. MIA endpoint paths — they are /api/v1/mia (singular) for CRUD; DELETE uses ?agent_config_id=... query param, not a path id.
  16. Live video on Zoom — not supported. Google Meet and Teams only.
  17. in_call_recording_timeout under 600 — live API rejects with HTTP 400. Minimum 600 seconds.
  18. Expecting create_bot to return HTTP 200 — it returns HTTP 201 Created (per OpenAPI and live-verified). status field is typically "Active".
  19. Expecting a post-call transcript from a streaming-only provider — streaming providers (meetstream_streaming, assemblyai_streaming, deepgram_streaming, jigsawstack_streaming, meeting_captions) return transcript_id: null and never populate bot_details.transcript_id or /transcriptions. The audio is saved but no transcript exists until you manually call POST /bots/{bot_id}/transcribe.
  20. Waiting for bot.done on a streaming-only bot — that event never fires for Path B. The terminal event is audio.processed. Handlers that block on bot.done will hang.
  21. Treating bot.error as a fatal event — it's only a streaming-provider warning (upstream auth issue). The bot continues recording; only live transcription is impacted.
  22. Waiting for bot.done after /transcribe — /transcribe is fire-and-forget. You get exactly one transcription.processed/transcription.failed, no bot.done follow-up.
  23. Relying on custom_attributes in /transcribe-triggered webhooks — that field is missing from /transcribe events (present on original lifecycle events). Correlate by bot_id instead.
  24. Polling /transcript/{tid}/get_transcript without a bound — on failure it returns HTTP 202 forever. Always check bot_details.TranscriptStatus (authoritative: "Success"/"Failed"/None) or watch for the transcription.failed webhook to break out.

Code Reference Files

Read these when building:

  • references/code-patterns-node.md — complete Node.js/TypeScript implementations
  • references/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 .md to 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.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.