# meetbot — full docs > meetbot is the open, EU-hosted, $0.30/hr meeting-bot API for Google Meet, Microsoft Teams, and Zoom. MIT-licensed SDKs, calendar-invite intake, $5 free credit on signup. This file is the concatenation of every page under https://meetbot.dev/docs, in source order. Use it for one-shot grounding when an agent can't follow links. For a short overview see https://meetbot.dev/llms.txt. --- ## Getting Started Source: https://meetbot.dev/docs > Dispatch your first meeting bot, verify a webhook, and pull your first recording. End-to-end in about five minutes. meetbot is a meeting-bot API. You hand us a Google Meet, Microsoft Teams, or Zoom URL; a bot joins the call, records per-speaker audio, the tab video, captions, and chat; and a signed webhook fires when the meeting ends. Files land in your S3-compatible bucket — never ours, unless you ask. Pricing is flat **$0.30/hr**, billed per minute. New accounts get **$5 of free credit** on signup; no card required to start. ## 1 · Install the SDK ```bash npm install @meetbot/sdk ``` ```bash pnpm add @meetbot/sdk ``` ```bash yarn add @meetbot/sdk ``` ```bash bun add @meetbot/sdk ``` The SDK is MIT-licensed; the source lives at [github.com/meetbot/sdk-js](https://github.com/meetbot/sdk-js). Python (`pip install meetbot`), Go (`go get github.com/meetbot/sdk-go`), and Rust SDKs ship with the same shape. ## 2 · Get an API key Sign in at [meetbot.dev/login](https://meetbot.dev/login) and create a key at **[/account/keys](https://meetbot.dev/account/keys)**. You get a `MEETBOT_API_KEY` (starts with `mb_live_…` for prod, `mb_test_…` for the sandbox) and a `MEETBOT_WEBHOOK_SECRET` for signing webhooks. ```bash export MEETBOT_API_KEY=mb_live_… export MEETBOT_WEBHOOK_SECRET=whsec_… ``` Keys are scoped to the team that created them. Revoke any key at `/account/keys` and any in-flight bot dispatched with that key keeps running, but no new dispatches will succeed. ## 3 · Dispatch your first bot ```ts title="dispatch.ts" import { createMeetbot } from "@meetbot/sdk"; const meetbot = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY! }); const job = await meetbot.dispatchBot({ url: "https://meet.google.com/abc-defg-hij", externalId: "session-42", webhooks: { onFinalize: "https://yours.example/hook" }, }); console.log(job.id, job.status); ``` A bot will join the meeting within 10–15 seconds. When the meeting ends — or you call `meetbot.leaveBot(job.id)` — we finalize the recording, upload it to your bucket, and POST the manifest to the webhook URL above. ## 4 · Verify the webhook signature We sign every webhook with HMAC-SHA256 over the raw body. The signature travels in the `x-meetbot-signature` header as `sha256=`. Reject anything that doesn't verify. ```ts title="webhook.ts" import { verifyWebhookSignature } from "@meetbot/sdk"; export async function POST(req: Request) { const raw = await req.text(); const ok = verifyWebhookSignature({ body: raw, header: req.headers.get("x-meetbot-signature"), secret: process.env.MEETBOT_WEBHOOK_SECRET!, }); if (!ok) return new Response("invalid signature", { status: 401 }); const event = JSON.parse(raw); // event.type === "recording.finalized" // event.data.manifestUri → s3://your-bucket/recordings//manifest.json return new Response("ok"); } ``` See [Webhooks: Signature Verification](/docs/basics/webhooks-verify) for working snippets in JavaScript, Python, and Go. ## What next Full end-to-end example: dispatch, listen, fetch the manifest, download per-speaker audio. Flat $0.30/hr, $5 free credit, Stripe portal at /account/billing. Single EU region today. US-East / US-West / APAC on the roadmap. The granular failure taxonomy (waiting_for_host, host_denied, etc.) — shipping in M1. --- ## CLI (npm + Claude Skill) Source: https://meetbot.dev/docs/ai-tools/cli > Install @meetbot/cli to dispatch bots, fetch recordings, manage keys, and forward webhooks from the terminal. Also distributable as an Anthropic Claude Skill so Claude.ai can drive the same operations from natural language. The `@meetbot/cli` npm package gives you the meetbot API on the command line. It also ships an [Anthropic Claude Skill](https://docs.claude.com/skills) so Claude (Claude.ai, Claude Code, Claude Agents) can run the same commands from natural-language prompts — same auth, same shape, no extra wiring. The package is MIT-licensed and lives at [github.com/meetbot-dev/meetbot](https://github.com/meetbot-dev/meetbot/tree/main/packages/cli). ## Use it from your terminal ### Install ```bash npm install -g @meetbot/cli # or: pnpm add -g @meetbot/cli # or: bun add -g @meetbot/cli ``` The binary is `meetbot`. One-off without installing: ```bash npx @meetbot/cli --help ``` ### Authenticate Browser flow: ```bash meetbot login ``` …or paste a key from [/account/keys](https://meetbot.dev/account/keys): ```bash meetbot login --token mb_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` …or set an env var (env wins over the saved token): ```bash export MEETBOT_API_KEY=mb_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` The token saves to `~/.config/meetbot/token` (mode 0600). ### Top commands ```bash # bots meetbot bot dispatch # URL defaults to clipboard contents meetbot bot get meetbot bot ls --status in_meeting meetbot bot watch # stream status until completion meetbot bot leave # finalize + leave meetbot bot pause|resume|stop meetbot bot chat "recording is on" # recordings meetbot recordings ls meetbot recordings get meetbot recordings download # → ./meetbot-/ meetbot recordings rm # GDPR-style delete # webhooks meetbot webhooks listen --port 4242 # forward prod webhooks to localhost meetbot webhooks verify --body file --secret $MEETBOT_WEBHOOK_SECRET # keys, calendar, usage, doctor meetbot keys ls / create / revoke meetbot calendar connect google meetbot usage meetbot doctor ``` Every command supports `--help` and `--json`. JSON mode emits one ndjson record per result on stdout — pipe to `jq`, `xargs`, etc. ### Output and exit codes | Exit | Meaning | | ---: | ------------------------------ | | 0 | success | | 1 | user error (bad input) | | 2 | server error / network failure | | 3 | not authenticated | | 4 | quota exceeded | CI scripts can branch on these without parsing stderr. ### Configuration | env var | default | purpose | | ------------------------- | ------------------------- | ------------------------------------------ | | `MEETBOT_API_KEY` | — | If set, overrides the saved token. | | `MEETBOT_API_BASE_URL` | `https://api.meetbot.dev` | Self-hosted / staging override. | | `MEETBOT_WEB_BASE_URL` | `https://meetbot.dev` | Used by `meetbot login`. | | `MEETBOT_API_TIMEOUT_MS` | `30000` | Per-request timeout. | | `MEETBOT_NO_UPDATE_CHECK` | unset | `1` in CI to skip the daily version check. | | `MEETBOT_WEBHOOK_SECRET` | — | Default for `meetbot webhooks verify`. | ## Use it from Claude The same package ships a [Claude Skill](https://docs.claude.com/skills) bundle at `skill/meetbot.skill/`. After `npm install -g @meetbot/cli` (so `meetbot` is on your PATH), upload the skill folder to Claude: 1. Open [claude.ai/settings/skills](https://claude.ai/settings/skills). 2. Click **Upload skill** and select the `meetbot.skill/` directory inside the installed package — typically at: ``` $(npm root -g)/@meetbot/cli/skill/meetbot.skill ``` 3. Confirm the trigger phrases (e.g. _"send a meetbot to..."_, _"record this google meet"_). Claude will then shell out to the `meetbot` CLI when you say things like: - _"Have a meetbot record this Meet: https://meet.google.com/abc-defg-hij"_ - _"List all my meetbots that are still in a meeting."_ - _"Make the meetbot leave call 01HX9N8K and send me the recording URL when it's done."_ - _"Verify this meetbot webhook signature against my secret."_ Claude Code users get the skill via the package's bundled `skill/` directory; no separate upload step. ## CLI vs MCP — which do I use? Both wrap the same orchestrator API. - **Use the [CLI](#install)** if you want to drive meetbot from your terminal, from a shell script, from CI/CD, or from Claude (via the Skill). It's the friendliest path and the best fit for the typical AI-agent product backend. - **Use the [MCP server](/docs/ai-tools/mcp)** if you want meetbot inside an IDE-embedded coding assistant — Cursor, Windsurf, or Cline — that speaks Model Context Protocol natively but doesn't shell out. Claude Code can use either; the Skill is preferred (richer, more robust, fewer permission prompts) but the MCP server still works. ## See also - [llms.txt — point your AI tool at our docs](/docs/ai-tools/llms-txt) - [Bot Quickstart](/docs/meeting-bots/quickstart) — what these commands call under the hood - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) — the algorithm `meetbot webhooks verify` implements --- ## llms.txt for AI Editors Source: https://meetbot.dev/docs/ai-tools/llms-txt > meetbot publishes /llms.txt and /llms-full.txt so AI coding tools can ground themselves on our docs in one fetch. Here's how to point Cursor, Claude Code, and ChatGPT at them. [llms.txt](https://llmstxt.org/) is a small Markdown file at the root of a website that tells AI agents what's available and where to look. It's the **`robots.txt` of the LLM era** — opt-in, structured, machine-readable. We publish two: - [`https://meetbot.dev/llms.txt`](https://meetbot.dev/llms.txt) — short overview with links to every doc page, the API surface, the SDK, and the MCP server. Fetch this first. - [`https://meetbot.dev/llms-full.txt`](https://meetbot.dev/llms-full.txt) — full Markdown of every doc page concatenated, for one-shot grounding in agents that can't follow links. Both are auto-generated from the Fumadocs source under `apps/web/content/docs/**` so they never drift. ## Point your tool at it ### Cursor (`@Docs`) `Cmd+Shift+P` → **"Add new docs"** → paste `https://meetbot.dev/llms-full.txt` → name it `meetbot`. Then in any chat: ``` @Docs meetbot how do I dispatch a bot to a Zoom URL? ``` Cursor will use the indexed doc to answer without fetching anything live. ### Claude Code In a turn, paste the URL — Claude will fetch it via WebFetch: ``` Read https://meetbot.dev/llms-full.txt and tell me how to verify a webhook. ``` Or add to your project's `CLAUDE.md`: ```markdown title="CLAUDE.md" When working with meetbot, the canonical reference is https://meetbot.dev/llms-full.txt — fetch it before answering questions about the meetbot API. ``` ### ChatGPT / Claude.ai (web) Just paste the URL into the chat. Both can fetch and parse `text/plain` Markdown documents. ### Custom RAG / agent ```bash curl https://meetbot.dev/llms-full.txt > meetbot-docs.md ``` Embed, chunk, index — the file is plain Markdown with stable section headers (one `## ` per page, plus a `Source:` line with the canonical URL). ## What's in `llms.txt` ```text # meetbot > meetbot is the open, EU-hosted, $0.30/hr meeting-bot API for > Google Meet, Microsoft Teams, and Zoom… ## Overview - [Getting Started](https://meetbot.dev/docs): … ## Basics - [Webhooks — Signature Verification](https://meetbot.dev/docs/basics/webhooks-verify): … - [Billing](https://meetbot.dev/docs/basics/billing): … ## Meeting Bots - [Bot Quickstart](https://meetbot.dev/docs/meeting-bots/quickstart): … - [Failure sub-codes](https://meetbot.dev/docs/meeting-bots/sub-codes): … ## API - [POST /api/v1/jobs](…): dispatch a bot - [GET /api/v1/jobs/:id](…): fetch one bot's status - [Webhook signing](…): HMAC-SHA256 over the raw body ## SDKs and tools - [@meetbot/sdk](https://www.npmjs.com/package/@meetbot/sdk) - [@meetbot/mcp-server](https://www.npmjs.com/package/@meetbot/mcp-server) ``` ## See also - [MCP server](/docs/ai-tools/mcp) — for direct tool access (vs. doc grounding) in Cursor / Claude Code / Windsurf / Cline. - [llmstxt.org](https://llmstxt.org/) — the spec. --- ## MCP Server (Cursor, Claude Code, Windsurf) Source: https://meetbot.dev/docs/ai-tools/mcp > Plug meetbot into your AI coding assistant. The @meetbot/mcp-server npm package exposes 17 tools — dispatch bots, fetch recordings, control live bots, verify webhooks — over the Model Context Protocol. Prefer the CLI? See [/docs/ai-tools/cli](/docs/ai-tools/cli). MCP is the alternative for IDE-embedded agents (Cursor, Windsurf, Cline) that speak Model Context Protocol natively but don't shell out. For most backend / scripting / Claude.ai use cases the CLI is friendlier. The meetbot MCP server lets your AI coding assistant operate the meetbot API on your behalf. Once installed, you can ask Cursor or Claude Code things like: - _"Send a meetbot to this Meet URL to record our standup."_ - _"List all bots that are currently in a meeting."_ - _"Get the recording manifest for the last bot we dispatched."_ - _"Have the bot post a 'recording is on' message in the chat."_ - _"Verify this incoming webhook signature against my secret."_ The package is MIT-licensed and lives at [github.com/meetbot-dev/mcp-server](https://github.com/meetbot-dev/mcp-server) (the `meetbot` GitHub org is taken; we publish under `meetbot-dev`). The npm name is `@meetbot/mcp-server`. ## Install You don't actually install anything. MCP clients spawn the server with `npx`, so all you do is point them at the right command + pass an API key. Get an API key first at [`/account/keys`](https://meetbot.dev/account/keys). New accounts get $5 of free credit. ## Cursor Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per-repo): ```json title=".cursor/mcp.json" { "mcpServers": { "meetbot": { "command": "npx", "args": ["-y", "@meetbot/mcp-server"], "env": { "MEETBOT_API_KEY": "mb_live_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` Restart Cursor. The 17 meetbot tools appear in **Settings → MCP**. ## Claude Code Add to `~/.claude.json` (global) or `.mcp.json` (per-project): ```json title=".mcp.json" { "mcpServers": { "meetbot": { "command": "npx", "args": ["-y", "@meetbot/mcp-server"], "env": { "MEETBOT_API_KEY": "mb_live_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` Or use the CLI: ```bash claude mcp add meetbot --env MEETBOT_API_KEY=mb_live_xxx -- npx -y @meetbot/mcp-server ``` ## Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json title="mcp_config.json" { "mcpServers": { "meetbot": { "command": "npx", "args": ["-y", "@meetbot/mcp-server"], "env": { "MEETBOT_API_KEY": "mb_live_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` ## Cline (VS Code) Open the Cline panel → **MCP Servers → Edit Configuration** and add: ```json title="cline_mcp_settings.json" { "mcpServers": { "meetbot": { "command": "npx", "args": ["-y", "@meetbot/mcp-server"], "env": { "MEETBOT_API_KEY": "mb_live_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` ## Tools exposed | Tool | Purpose | | -------------------------- | ---------------------------------------------------- | | `dispatch_bot` | Send a bot to record a meeting (Meet/Teams/Zoom). | | `get_bot` | Fetch one bot's status + metadata + manifest URL. | | `list_bots` | List bots, filterable by status/platform. | | `leave_bot` | Tell a bot to finalize and leave the meeting. | | `pause_recording` | Pause an in-progress recording. | | `resume_recording` | Resume a paused recording. | | `stop_recording` | Stop recording without leaving the meeting. | | `send_chat_message` | Post a chat message into the meeting. | | `get_recording` | Get the manifest + per-track media URLs. | | `get_transcript` | Get the post-meeting transcript (M6.1+). | | `list_jobs` | Alias of `list_bots`. | | `get_job` | Alias of `get_bot`. | | `delete_job` | Cancel a job (force-stops in-flight bots). | | `verify_webhook_signature` | Locally verify an incoming webhook (no API call). | | `get_consumer` | Read the consumer (account) the API key is bound to. | | `list_recordings` | List recent recordings. | | `delete_recording` | Permanently delete a recording (GDPR). | Tool descriptions are written to be picked up correctly by the LLM on the first try. If your assistant chooses the wrong tool for a clear request, [open an issue](https://github.com/meetbot-dev/mcp-server/issues). ## Configuration | env var | required | default | purpose | | ------------------------ | -------- | ------------------------- | -------------------------------------------------- | | `MEETBOT_API_KEY` | yes | — | Your meetbot API key (get one at /account/keys). | | `MEETBOT_API_BASE_URL` | no | `https://api.meetbot.dev` | Override for self-hosted orchestrators or staging. | | `MEETBOT_API_TIMEOUT_MS` | no | `30000` | Per-request timeout. | ## See also - [llms.txt — point your AI tool at our docs](/docs/ai-tools/llms-txt) - [Bot Quickstart](/docs/meeting-bots/quickstart) — what these tools call under the hood - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) — the `verify_webhook_signature` tool implements this exact algorithm --- ## API Reference Source: https://meetbot.dev/docs/api-reference > HTTP API for the meetbot orchestrator. OpenAPI 3.1 spec, Postman collection, endpoint table. The complete HTTP API reference is published as an **OpenAPI 3.1.0** spec served straight from this site. | Format | URL | | ------------------ | ---------------------------------------------------------------------- | | OpenAPI YAML | [`/openapi.yaml`](/openapi.yaml) | | OpenAPI JSON | [`/openapi.json`](/openapi.json) | | Postman collection | [`/meetbot.postman_collection.json`](/meetbot.postman_collection.json) | Drop the YAML / JSON URL into Insomnia, Postman, Bruno, Stoplight, or Redoc to get an interactive client. The Postman collection is pre-folded by tag (`bots`, `recordings`, `calendar`, `webhooks`, `admin`). ## Endpoints | Method | Path | Tag | Purpose | | -------- | ------------------------------------------------ | -------- | -------------------------------------------------------- | | `POST` | `/api/v1/jobs` | bots | Create + dispatch a meeting bot | | `GET` | `/api/v1/jobs/{id}` | bots | Fetch a job by id | | `DELETE` | `/api/v1/jobs/{id}` | bots | Cancel a job | | `POST` | `/api/v1/jobs/{id}/request-recording-permission` | bots | Ask the host for explicit consent | | `POST` | `/api/v1/intake/calendar-invite` | calendar | Receive a parsed calendar invite (worker → orchestrator) | | `POST` | `/api/v1/ingest` | ingest | Bot lifecycle ingest (orchestrator-internal) | | `POST` | `/api/v1/cli-auth/init` | cli-auth | Mint a device-flow nonce | | `POST` | `/api/v1/cli-auth/poll/{nonce}` | cli-auth | Poll for device-flow completion | | `POST` | `/api/v1/cli-auth/complete` | cli-auth | Finalize device-flow auth | | `GET` | `/api/v1/admin/host` | admin | Host metrics + bot counts (operator-only) | | `POST` | `/webhooks/stripe` | admin | Stripe webhook receiver (operator-only) | | `GET` | `/healthz` | admin | Liveness probe | ## Webhook events Outbound events the orchestrator POSTs to consumer-supplied URLs. v2 payloads always carry `version: 2` plus a `failure_code` enum on failure transitions; v1 payloads omit the version field and carry the legacy free-text `failure_reason` only. | Event | When | Notes | | ----------------------------------- | ------------------------------------------ | ------------------------------------------------ | ------ | | `job.status_changed` | every status transition | most events you'll handle | | `job.finalized` | manifest written | exactly once per successful job | | `job.failed` | terminal `failed` | always carries `failure_code` in v2 | | `bot.recording_permission_response` | host responded to the consent verb | `granted: true | false` | | `bot.competing_bot_detected` | spotted Otter / Fireflies / Read in roster | `leftMeeting` reflects `autoLeave.onBotDetected` | See the [webhook verification guide](/docs/basics/webhooks-verify) for HMAC signing + dedupe via `deliveryId`. ## Authentication Send your meetbot API key as a Bearer token: ```http GET /api/v1/jobs/5c3e9d8a-7c4e-4d11-b2cb-9d6e0c5e1234 HTTP/1.1 Host: api.meetbot.dev Authorization: Bearer mb_live_... ``` Issue + rotate keys in the dashboard. Revoked keys 401 immediately. ## Spec lifecycle The spec at `apps/web/openapi.yaml` is the source of truth. Today it's hand-maintained against `apps/orchestrator/src/server.ts` and the Zod schemas in `apps/orchestrator/src/routes/`. A future CI job will diff the route table on every PR and fail when the spec drifts — see [`/docs/api-reference/openapi`](/docs/api-reference/openapi) for the proposed regeneration cadence. --- ## OpenAPI spec Source: https://meetbot.dev/docs/api-reference/openapi > How to use the meetbot OpenAPI 3.1 spec — Postman, Insomnia, Redoc, code generation. The meetbot HTTP API is described as an [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0) document. Source of truth lives at [`apps/web/openapi.yaml`](https://github.com/meetbot/meetbot/blob/main/apps/web/openapi.yaml) in the repo and is mirrored to: - [`https://meetbot.dev/openapi.yaml`](/openapi.yaml) - [`https://meetbot.dev/openapi.json`](/openapi.json) - [`https://meetbot.dev/meetbot.postman_collection.json`](/meetbot.postman_collection.json) ## Use it with… ### Postman ``` File → Import → Link → https://meetbot.dev/meetbot.postman_collection.json ``` The collection arrives pre-folded by tag (`bots`, `recordings`, `calendar`, `webhooks`, `admin`). Set a collection variable `bearerToken = mb_live_…` and the requests authenticate automatically. ### Insomnia / Bruno / Hoppscotch Import the YAML or JSON URL above; all three handle OpenAPI 3.1 natively. ### Redoc (interactive HTML) ```html ``` ### Stoplight Elements ```html ``` ### Code generation The spec round-trips cleanly through the major generators: - [`openapi-typescript`](https://github.com/openapi-ts/openapi-typescript) → typed fetch client (the meetbot SDK already does this) - [`openapi-generator-cli`](https://github.com/OpenAPITools/openapi-generator-cli) → 50+ language clients - [`oapi-codegen`](https://github.com/oapi-codegen/oapi-codegen) → idiomatic Go client + server stubs ## What's in scope The spec covers every public HTTP endpoint the orchestrator exposes plus the outbound webhook events. Bot-side WebSocket / SSE channels (M1.1 control plane) live in their own [realtime spec](/docs/api-reference/) once that work lands. Internal endpoints (`/api/v1/ingest`, `/api/v1/admin/*`, `/webhooks/stripe`) are documented for completeness but tagged separately and gated by distinct auth schemes. ## Versioning The URL prefix is `/api/v1/`. We will not break v1 — additive changes only. When v2 ships, the spec will publish parallel v1 + v2 surfaces for at least 6 months before v1 sunsets. The webhook payload version is independent and tracked via the `x-meetbot-webhook-version` header (and the `version` field in v2+ envelopes). v1 webhook payloads remain accepted indefinitely; the deprecated mirror fields (`failure_reason`, `error`) are kept on v2 payloads so legacy consumers don't break. ## Regeneration cadence The spec is hand-maintained today. The intended workflow: 1. Engineer adds / changes a route handler under `apps/orchestrator/src/routes/`. 2. Same PR updates `apps/web/openapi.yaml`. 3. CI runs `pnpm openapi:validate` (swagger-cli validate) + `pnpm openapi:diff` (route enumeration vs spec) and fails the PR if either drifts. 4. On merge to `main`, a GitHub Action regenerates the JSON mirror + Postman collection and commits them via `[skip ci]`. That CI/Action work isn't built yet — see the [follow-up](https://github.com/meetbot/meetbot/issues) tracker. ## Reporting spec bugs Open an issue at [github.com/meetbot/meetbot](https://github.com/meetbot/meetbot/issues) with the operation id (e.g. `createJob`) and what's wrong. PRs editing `apps/web/openapi.yaml` directly are very welcome. --- ## Billing Source: https://meetbot.dev/docs/basics/billing > Flat $0.30 per recorded hour. $5 free credit on signup. Stripe-managed. No tiers, no per-platform pricing, no storage fees. One price, billed by the minute. ## The number | Item | Price | Notes | | ------------- | ------------------------ | ---------------------------------------------------------------------------------- | | **Recording** | **$0.30 / hour** | Billed per minute. Same price for Meet, Teams, and Zoom. | | Storage | **$0** | Recordings are uploaded directly to your S3-compatible bucket. We don't host them. | | Transcription | $0.10 / hour add-on (M6) | Or BYOK — bring your AssemblyAI/Deepgram/Whisper key, no markup. | | Signup credit | **$5** | ≈ 16.6 hours of recording. No card required to claim. | | Calendar API | $0 | Free always. | That's the whole pricing page. No "Pro" tier, no per-bot fees, no setup, no minimums. We win on transparency and we mean it — see [`meetbot.dev/#pricing`](https://meetbot.dev/#pricing) for the live calculator. ## How metering works Every bot run emits one usage event when it finalizes: ``` event: bot.usage_recorded job_id: job_01HXY… seconds: 4253 hours_billed: 1.181 (rounded to nearest minute) amount_usd: 0.354 ``` Events stream into Stripe via the [Billing Meters API](https://docs.stripe.com/billing/subscriptions/usage-based) under our `recording_hours` meter. Your invoice at month-end aggregates them; you can audit raw events any time at [`/account/billing`](https://meetbot.dev/account/billing) → "Usage events". A bot that connects but never gets admitted is **not billed** (we'd be charging you for a failure). A bot that records for less than 60 seconds is also not billed. ## Free credit New accounts get **$5** of credit applied to the first invoice. It covers the first ~16 recorded hours. No card required to start; you'll only hit a card prompt when usage exceeds the credit and an invoice would otherwise generate. If you exhaust the credit and never add a card, in-flight bots finish but new dispatches return `402 PaymentRequired` with `code: "credit_exhausted"`. Add a card at `/account/billing` to resume. ## Stripe portal We use Stripe's hosted customer portal. From [`/account/billing`](https://meetbot.dev/account/billing) you can: - Update card / SEPA mandate / ACH method - Download invoices (PDF + CSV) - Update billing email and tax ID (used for EU VAT reverse-charge) - Cancel — no contracts, no notice period Self-serve everything. No "contact sales to cancel" tricks. ## Where to see usage | Where | What you see | | --------------------------------------------------------------- | -------------------------------------------------------- | | [`/account`](https://meetbot.dev/account) | Today's usage + month-to-date + current invoice estimate | | [`/account/billing`](https://meetbot.dev/account/billing) | Stripe portal, invoices, usage events feed | | [`/account/recordings`](https://meetbot.dev/account/recordings) | Per-bot duration + cost line | ## Volume + enterprise Above 1,000 hours/month we'll cut a custom rate (typically 20–35% off, case by case). Email [sales@meetbot.dev](mailto:sales@meetbot.dev) to discuss. **Honest scope on enterprise extras.** We do not have SOC 2 today, no HIPAA / BAA template, no published uptime SLA. SOC 2 Type 1 is on the M5 roadmap (target Q4 2026, not committed); BAA template would follow that audit. We can send a DPA template on request — it's a soft commitment, not a published certified document. If your procurement process requires any of these on day one, talk to Recall or another SOC 2-certified vendor; we'll tell you up front rather than waste your time. ## Pricing cadence We commit to **public price-change notice** at least 60 days before any increase. The pricing page footer at `meetbot.dev/#pricing` carries a `prices last verified YYYY-MM-DD` stamp; if you ever see a stale stamp, [file an issue](https://github.com/meetbot/meetbot/issues) — the docs repo is public and PRs are welcome. ## See also - [Failure sub-codes](/docs/meeting-bots/sub-codes) — billed vs. not-billed status taxonomy - [Regions](/docs/basics/regions) — pricing is identical across regions --- ## Regions Source: https://meetbot.dev/docs/basics/regions > Where meetbot runs today, where it will run, and how to think about data residency until we ship multi-region. We run a single hosted region today. Honest scope first; expansion second. ## Today (May 2026) | Region | Status | Base URL | Hosted in | | ------ | ------ | ------------------------- | ------------------------------ | | **EU** | GA | `https://api.meetbot.dev` | Hetzner — Falkenstein, Germany | Bots, the orchestrator, the Postgres database, and your recording bucket proxy all live in the EU region. Customer media is uploaded directly from the bot to **your** S3-compatible bucket — wherever you host it, you keep it. We never persist the recording bytes outside your bucket. GDPR: data processing addendum on request — email [legal@meetbot.dev](mailto:legal@meetbot.dev). We are the data processor; you remain the controller for any meeting content. ## On the roadmap | Region | ETA | Planned base URL | Triggered by | | ------- | ------- | --------------------------------- | ------------------------------------------------------- | | US-East | Q4 2026 | `https://us-east.api.meetbot.dev` | First US enterprise customer with residency requirement | | US-West | TBD | `https://us-west.api.meetbot.dev` | Latency-sensitive Zoom Webinars workloads | | APAC | TBD | `https://apac.api.meetbot.dev` | First APAC customer at meaningful volume | We will not pre-build regions on speculation. Each region adds operational surface (Postgres, observability, on-call rotation) — we add the infrastructure when a paying customer needs it. ## How regions will work once they exist Each region will run an **independent** orchestrator + Postgres + bot fleet. Bots dispatched against `https://api.meetbot.dev` will continue to land in EU; pin a region by hitting its base URL directly: ```ts import { createMeetbot } from "@meetbot/sdk"; // Default: EU region const meetbot = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY! }); // Future: pin to US-East const meetbotUs = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY!, baseUrl: "https://us-east.api.meetbot.dev", }); ``` API keys will be valid across regions; data (recordings, transcripts, calendar grants) stays in the region where it was created. There is no plan for a global control plane that forwards requests across regions — each region is its own product surface. ## What stays single-region forever - **Stripe billing** — one account, one global tax setup. No per-region split. - **Customer dashboard** at `meetbot.dev/account/*` — UI is global; data fetched from the region the bot ran in. - **Auth** (better-auth + magic link / Google) — one identity per email. ## See also - [Billing](/docs/basics/billing) — pricing is identical across regions once they ship; no US-vs-EU surcharge. - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) — the signing key is global per team. --- ## Webhooks — Signature Verification Source: https://meetbot.dev/docs/basics/webhooks-verify > Every meetbot webhook is signed with HMAC-SHA256. Reject anything that doesn't verify. JavaScript, Python, and Go snippets included. Every webhook we send carries an `x-meetbot-signature` header in the form `sha256=`. The hex is `HMAC-SHA256(webhook_secret, raw_request_body)`. You **must** verify this signature before parsing the body. Anyone with your webhook URL could otherwise POST garbage at it; the signature is what proves the request came from us. ## The recipe 1. Read the **raw request body** as bytes — do not parse JSON first, the signature is computed over the exact bytes we sent. 2. Read the `x-meetbot-signature` header. 3. Compute `HMAC-SHA256` of the body using your webhook secret. 4. Compare the two using a **constant-time** comparison (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python, `hmac.Equal` in Go). The webhook secret is at [`/account/keys`](https://meetbot.dev/account/keys) under the "Webhook signing secret" row. It starts with `whsec_…`. ## JavaScript / Node ```ts title="webhook.ts" import crypto from "node:crypto"; export function verifyMeetbotWebhook( rawBody: string | Buffer, signatureHeader: string | null, secret: string, ): boolean { if (!signatureHeader?.startsWith("sha256=")) return false; const provided = Buffer.from(signatureHeader.slice("sha256=".length), "hex"); const expected = crypto.createHmac("sha256", secret).update(rawBody).digest(); return provided.length === expected.length && crypto.timingSafeEqual(provided, expected); } ``` In an Express / Fastify / Next.js route: ```ts // Next.js App Router — req.text() returns the *raw* body string. export async function POST(req: Request) { const raw = await req.text(); if ( !verifyMeetbotWebhook( raw, req.headers.get("x-meetbot-signature"), process.env.MEETBOT_WEBHOOK_SECRET!, ) ) { return new Response("invalid signature", { status: 401 }); } const event = JSON.parse(raw); // ... handle event.type return new Response("ok"); } ``` The `@meetbot/sdk` package re-exports this as `verifyWebhookSignature` so you don't have to copy-paste it. ## Python ```python title="webhook.py" import hmac import hashlib def verify_meetbot_webhook(raw_body: bytes, signature_header: str | None, secret: str) -> bool: if not signature_header or not signature_header.startswith("sha256="): return False provided = bytes.fromhex(signature_header[len("sha256="):]) expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest() return hmac.compare_digest(provided, expected) ``` Flask handler: ```python from flask import request, abort @app.post("/webhook/meetbot") def meetbot_hook(): raw = request.get_data() # bytes, not request.json if not verify_meetbot_webhook( raw, request.headers.get("X-Meetbot-Signature"), os.environ["MEETBOT_WEBHOOK_SECRET"] ): abort(401) event = request.get_json() # ... return "ok" ``` ## Go ```go title="webhook.go" package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strings" ) func VerifyMeetbotWebhook(rawBody []byte, signatureHeader, secret string) bool { if !strings.HasPrefix(signatureHeader, "sha256=") { return false } provided, err := hex.DecodeString(signatureHeader[len("sha256="):]) if err != nil { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) return hmac.Equal(provided, mac.Sum(nil)) } ``` `net/http` handler: ```go func handler(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) if !VerifyMeetbotWebhook(raw, r.Header.Get("X-Meetbot-Signature"), os.Getenv("MEETBOT_WEBHOOK_SECRET")) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // parse + handle } ``` ## Curl test from the dashboard The dashboard has a **Resend** button on each delivered webhook at `/account/recordings/:id`. Use it after deploying a verifier change to sanity-check end-to-end without waiting for the next real meeting. ## Common gotchas - **Body parser ate the bytes.** Express's `express.json()` middleware consumes the stream and gives you `req.body` already parsed. You'll need `express.raw({ type: "application/json" })` on this route specifically. - **You re-stringified the JSON.** `JSON.stringify(JSON.parse(raw))` is not byte-for-byte equal to the original — key ordering and whitespace diverge. Always hash the bytes you received on the wire. - **You compared with `===` or `==`.** Use a constant-time compare; a timing oracle on a 32-byte hex string is well within an attacker's budget. - **Multiple signatures.** We currently send exactly one `x-meetbot-signature`. When we add a second header for key rotation (planned M2), it will be CSV-formatted (`sha256=…,sha256=…`) — accept if **any** entry verifies. --- ## Bot Features (coming soon) Source: https://meetbot.dev/docs/bot-features > Recording, separate audio, separate video, output media, chat send. Shipping with M1 bot control API. This section will document the per-feature surface of the bot: - Recording controls (start / pause / resume / stop) - Per-speaker audio + tab video + screenshare separation - Output media: bot speaks audio, displays an image, screenshares an MP4 - Chat send + pin participant + screenshot capture Shipping with the M1 bot control API. Watch [the roadmap](https://github.com/meetbot/meetbot/blob/main/docs/ROADMAP.md) for progress. --- ## Calendar (coming soon) Source: https://meetbot.dev/docs/calendar > OAuth-based recording rules — Google Calendar + Microsoft Outlook. Shipping in M2. The Calendar API will let your end-users connect their own Google or Outlook calendar via OAuth and configure rules like: - "Record every meeting with `@customer.com`" - "Skip 1:1s with my manager" - "Record only meetings I host" You hand us the OAuth grant; we sync events every five minutes and schedule bots per the rules. No need for you to implement OAuth. Shipping in M2 (Jun–Jul 2026). Today, use the email-invite intake (`bot@meetbot.dev` on a calendar invite) for similar behavior. --- ## Bot Quickstart Source: https://meetbot.dev/docs/meeting-bots/quickstart > End-to-end walkthrough — dispatch a bot to a Google Meet URL, listen for the finalize webhook, fetch the manifest, and download per-speaker audio. This is the full path from "I have a meeting URL" to "I have audio files on disk". Real working code, no pseudo-snippets. ## Set up env ```bash export MEETBOT_API_KEY=mb_live_… # /account/keys export MEETBOT_WEBHOOK_SECRET=whsec_… # same page export MEETBOT_MANIFEST_BUCKET=s3://your-bucket # whatever you configured at signup ``` ## Dispatch the bot ```ts title="dispatch.ts" import { createMeetbot } from "@meetbot/sdk"; const meetbot = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY! }); const job = await meetbot.dispatchBot({ url: "https://meet.google.com/abc-defg-hij", externalId: "demo-2026-05-09", webhooks: { onFinalize: "https://yours.example/hook/meetbot" }, // Optional — you can let the bot figure out a sensible default. joinAt: undefined, // join immediately leaveBehavior: "when-host-leaves", // or "after-silence:300s" or "explicit" }); console.log("dispatched", job.id, "→", job.status); // dispatched job_01HXY1AB7M2QNTWA9YQRH5JEP6 → queued ``` The bot transitions through these states: ``` queued → joining → in_call → recording → finalizing → completed ↘ failed (with code, see sub-codes) ``` You can poll `meetbot.getJob(job.id)` if you want, but the recommended path is to wait for the webhook. ## Listen for the webhook ```ts title="app/api/webhook/meetbot/route.ts" import { verifyWebhookSignature } from "@meetbot/sdk"; export async function POST(req: Request) { const raw = await req.text(); const ok = verifyWebhookSignature({ body: raw, header: req.headers.get("x-meetbot-signature"), secret: process.env.MEETBOT_WEBHOOK_SECRET!, }); if (!ok) return new Response("invalid signature", { status: 401 }); const event = JSON.parse(raw) as MeetbotEvent; if (event.type === "recording.finalized") { // Kick off your post-meeting pipeline. ACK fast — we time out at 10s // and retry up to 5 times with exponential backoff. queueProcess(event.data); } return new Response("ok"); } interface MeetbotEvent { type: "recording.finalized" | "bot.failed" | "bot.left"; data: { jobId: string; externalId: string; durationSeconds: number; manifestUri: string; // s3://bucket/recordings//manifest.json failureCode?: string; // present on bot.failed — see sub-codes }; } ``` See [Webhooks: Signature Verification](/docs/basics/webhooks-verify) for Python and Go versions of the verifier. ## Fetch the manifest The manifest is a single JSON file in your bucket that points at every artifact for the meeting: ```ts title="fetch-manifest.ts" import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "eu-central-1" }); async function fetchManifest(manifestUri: string) { // manifestUri looks like: s3://your-bucket/recordings/job_01HXY…/manifest.json const [, , bucket, ...keyParts] = manifestUri.split("/"); const key = keyParts.join("/"); const obj = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const body = await obj.Body!.transformToString(); return JSON.parse(body) as Manifest; } interface Manifest { jobId: string; startedAt: string; endedAt: string; durationSeconds: number; participants: Array<{ id: string; displayName: string; speakerLabel: string }>; artifacts: { tabVideo: { uri: string; mimeType: "video/webm"; bytes: number }; perSpeakerAudio: Array<{ participantId: string; uri: string; mimeType: "audio/ogg"; bytes: number; }>; captions?: { uri: string; mimeType: "text/vtt" }; chat?: { uri: string; mimeType: "application/json" }; }; } ``` ## Download per-speaker audio ```ts title="download-audio.ts" import fs from "node:fs"; import path from "node:path"; async function downloadPerSpeakerAudio(manifest: Manifest, outDir: string) { await fs.promises.mkdir(outDir, { recursive: true }); await Promise.all( manifest.artifacts.perSpeakerAudio.map(async (track) => { const [, , bucket, ...keyParts] = track.uri.split("/"); const obj = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: keyParts.join("/") })); const participant = manifest.participants.find((p) => p.id === track.participantId)!; const filename = `${participant.speakerLabel}-${participant.displayName.replace(/\s+/g, "_")}.ogg`; const stream = obj.Body as NodeJS.ReadableStream; await new Promise((resolve, reject) => { stream .pipe(fs.createWriteStream(path.join(outDir, filename))) .on("finish", resolve) .on("error", reject); }); }), ); } const manifest = await fetchManifest(event.data.manifestUri); await downloadPerSpeakerAudio(manifest, `./recordings/${manifest.jobId}`); ``` You now have one `.ogg` file per participant, perfectly diarized — every file contains _only_ that speaker's audio with silence where they weren't talking. Concatenate, mix, transcribe per-speaker, whatever you want. ## What you now have on disk ``` ./recordings/job_01HXY1AB7M2QNTWA9YQRH5JEP6/ ├── A-Alice_Chen.ogg ← per-speaker audio ├── B-Bob_Patel.ogg ├── C-Carla_Mendes.ogg ├── tab.webm ← the full meeting tab capture ├── captions.vtt ← timestamped, native captions └── chat.json ← in-call chat messages ``` ## Things you'll want to add for production - **Idempotency on the webhook** — we may retry; key off `event.data.jobId + event.type`. - **Replay protection** — store delivered webhook IDs (`event.id`) for ~10 minutes; reject duplicates inside that window. - **Failure handling** — see [sub-codes](/docs/meeting-bots/sub-codes) for the taxonomy. `host_denied_admission` is fundamentally different from `internal_error` and your code should treat them differently. ## See also - [Failure sub-codes](/docs/meeting-bots/sub-codes) - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) - [Billing](/docs/basics/billing) — the bot you just dispatched cost about $0.01 if it ran for two minutes. --- ## Failure Sub-codes Source: https://meetbot.dev/docs/meeting-bots/sub-codes > The granular failure taxonomy returned on job.failed webhooks. Tells you whether to retry, escalate, or surface to your end-user. When a bot can't complete its job, the `job.failed` webhook (envelope `version: 2`) carries both a structured `failure_code` and a human-readable `failure_detail`: ```json { "deliveryId": "dlv_01HXZ…", "event": "job.failed", "emittedAt": "2026-05-09T14:00:00.000Z", "attempt": 1, "version": 2, "data": { "jobId": "job_01HXY…", "status": "failed", "failure_code": "host_denied_admission", "failure_detail": "host clicked 'Deny' after 47s in waiting room", "failure_reason": "host clicked 'Deny' after 47s in waiting room" } } ``` `failure_reason` is mirrored from `failure_detail` for v1 receivers that pre-date M1.2 — see the migration section below. New code MUST branch on `failure_code` and surface `failure_detail` to humans. The HTTP request also carries the `x-meetbot-webhook-version: 2` header so middleware can route on the version without parsing the body. The matching SDK type (`@meetbot/sdk`): ```ts import type { FailureCode, WebhookEvent } from "@meetbot/sdk"; type FailureCode = | "waiting_for_host" | "meeting_not_found" | "host_denied_admission" | "removed_from_meeting" // canonical for Recall.ai's `bot_kicked` too | "meeting_ended" | "internal_error" | "dispatch_timeout" | "network_loss" | "recording_disabled" | "lobby_timeout" | "auth_failure" | "quota_exceeded"; ``` ## The codes | Code | Description | Billed | Recommended action | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------: | ---------------------------------------------------------------------------------------------------- | | `waiting_for_host` | Bot is in the waiting room and the host hasn't joined within the configured timeout. | No | Retry later or notify your user. | | `meeting_not_found` | The URL resolved to a meeting that does not exist (e.g. mistyped Meet code). | No | Surface to the user as "URL invalid". | | `host_denied_admission` | Host actively denied the bot from joining. | No | Surface as "host denied" — do not auto-retry. | | `removed_from_meeting` | Bot was kicked mid-call. Canonical code for both passive disconnects and explicit "remove participant" actions; we deliberately collapsed Recall.ai's `bot_kicked` into this one. | Yes (for recorded portion) | Treat as a successful partial recording — manifest is still uploaded. | | `meeting_ended` | Meeting ended before the bot could join (URL was already-stale at dispatch). | No | Don't retry. | | `internal_error` | Something on our side broke. | No | Safe to retry; if it happens repeatedly, [open an issue](https://github.com/meetbot/meetbot/issues). | | `dispatch_timeout` | Bot container failed to start within the deadline (60s default). | No | Auto-retry once is reasonable. | | `network_loss` | Bot lost network mid-call and could not reconnect within the grace window. | Yes (partial) | Manifest still uploaded for the recorded portion. | | `recording_disabled` | Host or org policy disabled recording (Zoom "recording locked", Meet "host management" toggles). | No | Surface to user; we cannot bypass. | | `lobby_timeout` | Admitted to lobby but never to the actual meeting room within the timeout. | No | Retry later. | | `auth_failure` | Bot's signed-in credentials (Zoom OAuth / Google login pool) were invalid or revoked. | No | Reconnect the OAuth integration. | | `quota_exceeded` | Your account is out of credit and has no card on file. | No | Add a card at `/account/billing`. | ## Billed vs not-billed The `billed` field tells you definitively whether the run consumed credit. The rule is simple — **if the bot recorded at least 60s of content, the recorded portion is billed.** Codes like `host_denied_admission`, `waiting_for_host`, `meeting_not_found`, and any pre-join failure are _always_ not-billed — we don't charge for failures that produced no audio. When `billed: true` on a failure, you also get the partial manifest at `event.data.manifestUri` — the recording up to the moment of disconnect is uploaded normally. ## Pattern: branch on code ```ts import type { WebhookEvent } from "@meetbot/sdk"; async function onJobFailed(event: Extract) { const { failure_code, failure_detail, jobId } = event.data; switch (failure_code) { case "host_denied_admission": case "recording_disabled": // User-facing — do not retry, tell the user. await notifyUserBotBlocked(jobId, failure_detail); return; case "waiting_for_host": case "lobby_timeout": case "dispatch_timeout": // Transient — retry with backoff. await scheduleRetry(jobId, { delaySec: 60 }); return; case "auth_failure": case "quota_exceeded": // Account-level — page on-call. await pageOnCall({ jobId, failure_code, failure_detail }); return; case "internal_error": // Our problem. Log, ack, move on. log.warn("meetbot internal_error", { jobId, failure_detail }); return; default: // removed_from_meeting / network_loss / meeting_ended → recording // is partial-or-final, treat like a normal job.finalized event. return; } } ``` ## Migration from `failure_reason` (v1 → v2) Pre-M1.2 webhooks shipped only a free-text `failure_reason`. v2 keeps emitting that field — populated from the same string as `failure_detail` — so v1 receivers continue to work without code changes during the cut-over. The header `x-meetbot-webhook-version` and the envelope's `version` field both expose the schema version so consumers can branch: ```ts const isV2 = event.version === 2; const code = isV2 ? event.data.failure_code : mapLegacyReason(event.data.failure_reason); ``` We'll remove `failure_reason` from outbound payloads in M3 (Aug 2026) after a 60-day announcement window. ## See also - [Bot Quickstart](/docs/meeting-bots/quickstart) — where the `bot.failed` webhook fits in the lifecycle. - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) — failure events carry the same signature scheme as success events. --- ## Discord Source: https://meetbot.dev/docs/per-platform/discord > How to dispatch a meetbot bot into a Discord voice channel — Discord application setup, OAuth invite, and per-platform quirks. Discord is structurally different from Meet, Teams, and Zoom. The other platforms have no meeting-bot API, so meetbot drives a real Chrome browser through the join UI. Discord has a first-class **[Bots API](https://discord.com/developers/docs/intro)** + voice gateway, so meetbot connects directly as a Discord bot — no browser, no DOM scraping, no anti-bot fingerprint dance. ## What you get - **Per-speaker Opus tracks** — `audio..ogg` per user, written straight from Discord's voice gateway (Discord's negotiated codec IS Opus, so no transcoding). Multiple utterances by the same user rotate as `audio.-2.ogg`, `audio.-3.ogg`, … — same convention as Recall.ai's per-speaker artifacts. - **Roster events** in the manifest: `participant.join` / `participant.leave` for every voice-channel transition. - **Chat capture** (optional): point us at the text channel paired with the voice channel and we capture every message into `chat.jsonl`. - **Auto-leave** on the same four thresholds as every other platform — silence, solo, max-duration, entry-delay (no humans joined within N seconds). ## What you don't get - **No live captions.** Discord doesn't ship server-side speech-to-text. The customer can BYO transcription against the per-speaker Opus tracks (Whisper, Deepgram, AssemblyAI — anything that takes Opus or transcoded WAV). - **No per-call display name.** The bot uses whatever name you registered for the Discord application; meetbot's `displayName` field is silently ignored for Discord. To rename the bot, rename the application in the Discord Developer Portal. - **No video.** Discord voice channels are audio-only by default (stage channels and screenshare add video, but the voice-receive pipeline only exposes Opus). ## Customer setup (one-time) You'll need a Discord application + bot token. **Five steps**, takes ~5 minutes: ### 1. Create a Discord application Visit [https://discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application**. Pick a name (this is what shows up in the voice channel — pick something like `Acme Notetaker`). Save. ### 2. Add a Bot In the app's settings, navigate to **Bot** in the left sidebar. Click **Reset Token** (or **Add Bot** if it's a fresh app). Discord shows the bot token ONCE — copy it now and paste it into your meetbot dispatch config. If you lose it, reset and copy again. While you're on this page, scroll down and **enable the "Message Content Intent"** under Privileged Gateway Intents (only required if you want chat capture). ### 3. Generate an OAuth2 invite URL Navigate to **OAuth2 → URL Generator** in the sidebar. Under **Scopes**, check `bot`. A second permissions panel appears below; check: - `View Channels` - `Connect` - `Speak` - `Use Voice Activity` - `Read Message History` (only needed for chat capture) The page shows the integer permission bitmask at the bottom — should be `36702208` if you ticked exactly the boxes above. Copy the **Generated URL** at the bottom of the page. ### 4. Add the bot to your Discord server Open the URL from step 3 in a browser. Discord asks which server to add the bot to. Pick the target server. **You must be a server admin or the server owner** to do this. ### 5. Look up the guild + voice-channel ids In the Discord client, open **User Settings → Advanced** and toggle **Developer Mode** ON. Now right-click any server icon → **Copy ID** to get the guild id; right-click any voice channel → **Copy ID** to get the channel id. Both are 17-19 digit "snowflake" numbers. If you want chat capture, also right-click the text channel paired with the voice channel and copy that id. ## Dispatch via the SDK ```typescript import { MeetBot } from "@meetbot/sdk"; const meetbot = new MeetBot({ apiKey: process.env.MEETBOT_API_KEY }); const job = await meetbot.dispatch({ externalId: "standup-2026-05-09", platform: "discord", // The synthetic discord:// scheme keeps every platform URL-keyed // in the API. The bot token + ids on the `discord` block below // do the actual work. meetingUrl: "discord://YOUR_GUILD_ID/YOUR_VOICE_CHANNEL_ID", discord: { botToken: process.env.DISCORD_BOT_TOKEN!, guildId: "YOUR_GUILD_ID", channelId: "YOUR_VOICE_CHANNEL_ID", // Optional — omit to skip chat.jsonl capture. textChannelId: "YOUR_TEXT_CHANNEL_ID", }, webhooks: { onFinalize: "https://api.example.com/meetbot-webhook", }, autoLeave: { // Default Discord auto-leave: bail if nobody joined the voice // channel within 5 minutes of the bot connecting. afterEntryDelaySeconds: 300, // 4-hour cap. afterMaxSeconds: 14_400, }, }); ``` Python is identical: ```python from meetbot import MeetBot, DiscordConfig meetbot = MeetBot(api_key=os.environ["MEETBOT_API_KEY"]) job = meetbot.dispatch( external_id="standup-2026-05-09", platform="discord", meeting_url="discord://YOUR_GUILD_ID/YOUR_VOICE_CHANNEL_ID", discord=DiscordConfig( bot_token=os.environ["DISCORD_BOT_TOKEN"], guild_id="YOUR_GUILD_ID", channel_id="YOUR_VOICE_CHANNEL_ID", text_channel_id="YOUR_TEXT_CHANNEL_ID", ), ) ``` ## Manifest shape A completed Discord job's manifest looks like this: ```json { "platform": "discord", "exitReason": "alone_in_room", "speakers": [ { "id": "287541432954880001", "displayName": "alice" }, { "id": "354722510892302337", "displayName": "bob" } ], "tracks": [ { "trackId": "287541432954880001", "kind": "per-speaker", "format": "opus", "speakerId": "287541432954880001", "uri": "s3://bucket/runs/.../audio.287541432954880001.ogg", "bytes": 123456, "encoding": { "mimeType": "audio/ogg; codecs=opus" } } ], "chatUri": "s3://bucket/runs/.../chat.jsonl" } ``` ## Security notes - The Discord bot token is treated as a secret. We accept it on dispatch, store it in the job's `metadata.discord.botToken`, and forward it to the bot container as the `DISCORD_BOT_TOKEN` env var. The token is **stripped from every API response** — the `metadata.discord` echoed in `GET /api/v1/jobs/:id` and webhook payloads contains `guildId`, `channelId`, and `textChannelId` only, never the token. - We recommend rotating the token if any meetbot job using it ever lands in a state where the customer can't account for the container's logs (e.g. you fired a dispatch at the wrong account). Rotation is a single click in the Discord Developer Portal. - Only grant the bot the permissions listed in step 3. The OAuth2 generator's "Administrator" tickbox is convenient but vastly over-scoped — `Connect` + `Speak` + `View Channels` is everything the meetbot adapter needs. ## Pricing Discord meetings count against your meetbot quota the same way Meet/Teams/Zoom do — `$0.30/hour` of bot-time after the 10 free-hours-per-month tier. The `failure_code` taxonomy is identical: bad bot token surfaces as `auth_failure`, no humans joined within entry-delay surfaces as `lobby_timeout`, and so on. --- ## Per-platform Source: https://meetbot.dev/docs/per-platform > Platform-specific quirks — Meet, Teams, Zoom (Web + Linux SDK), Webex, Discord, Jitsi, Whereby. Each meeting platform behaves differently. This section covers, per platform: how authentication works, which features the bot can control, known quirks, and any platform-specific webhook payload extras. **Live today:** Google Meet, Microsoft Teams, Zoom Web, [Cisco Webex](/docs/per-platform/webex), [Whereby](/docs/per-platform/whereby), [Jitsi Meet](/docs/per-platform/jitsi), [Discord](/docs/per-platform/discord). **Shipping in M5:** Zoom Linux Meeting SDK (replaces Zoom Web for paid hosts). Common per-platform plumbing (lobby polling, mic/cam toggles, captions toggle, roster snapshot, competing-bot detection, auto-leave) is shared across every browser-automation adapter — the per-platform pages call out only the differences. For the integration walkthrough that's the same regardless of platform, see [Bot Quickstart](/docs/meeting-bots/quickstart). --- ## Jitsi Meet Source: https://meetbot.dev/docs/per-platform/jitsi > Joining Jitsi Meet rooms — public meet.jit.si and self-hosted instances. Uses lib-jitsi-meet's JS API for clean roster + admit signals. The `jitsi` adapter joins Jitsi Meet conferences, both the public instance at `meet.jit.si` and any self-hosted Jitsi deployment. Where possible the adapter uses lib-jitsi-meet's JS API on `window.JitsiMeetJS` (and `window.APP.conference`) instead of DOM scraping — this is stable across Jitsi versions and locale changes. ## Supported URL patterns | Pattern | Notes | | ----------------------------- | ---------------------------------- | | `https://meet.jit.si/` | Public meet.jit.si | | `https:///` | Self-hosted (with `serverUrl` set) | ## Public meet.jit.si ```bash curl -X POST https://api.meetbot.dev/api/v1/jobs \ -H "authorization: Bearer $MEETBOT_API_KEY" \ -H "content-type: application/json" \ -d '{ "externalId": "jitsi-demo-2026-05-09", "meetingUrl": "https://meet.jit.si/myroom-2026", "platform": "jitsi", "displayName": "meetbot", "webhooks": {"onFinalize": "https://yours.example/hook/meetbot"} }' ``` ## Self-hosted Jitsi EU education customers often run their own Jitsi deployment. Pass the `serverUrl` in the dispatch metadata, OR set `JITSI_SERVER_URL` on the orchestrator to apply the rewrite globally: ```bash # Per-job override: curl -X POST https://api.meetbot.dev/api/v1/jobs \ -H "authorization: Bearer $MEETBOT_API_KEY" \ -H "content-type: application/json" \ -d '{ "externalId": "uni-lecture-2026-05-09", "meetingUrl": "https://meet.jit.si/lecture-room-cs101", "platform": "jitsi", "metadata": {"jitsi": {"serverUrl": "https://jitsi.eduuni.example"}} }' # Global override (orchestrator env): JITSI_SERVER_URL=https://jitsi.eduuni.example ``` When `serverUrl` is set, the bot rewrites the URL's host and protocol but keeps the room path. So `https://meet.jit.si/cs101` → `https://jitsi.eduuni.example/cs101`. ## Lobby behavior - **Public rooms** without the lobby module enabled have no waiting room — the bot joins immediately. - **Lobby-enabled rooms** (Jitsi 8.0+ with the moderated-meetings feature) require moderator approval. The bot polls the `conference.joined` event from JitsiMeetJS to detect admit. - **Password-protected rooms** are NOT supported today — the bot has no password mechanism. Such jobs fail with a `meeting_inaccessible` error and `failure_code: auth_failure`. (M2 ticket to add a per-job password field.) ## Captions Jitsi exposes captions ("Subtitles") via the More menu when the deployment has the captions service (Jigasi) configured. The adapter prefers the programmatic toggle (`APP.UI.toggleSubtitles()`) and falls back to clicking the menu item. On deployments without Jigasi, the toggle silently no-ops and the bot's per-speaker audio capture remains the canonical source for downstream STT. ## Roster + competing-bot detection The adapter prefers the JS API (`APP.conference.listMembers().map(m => m.getDisplayName())`) over DOM scraping for the roster snapshot — cleaner, locale-independent, no race with tile-mount timing. The same `evaluateAutoLeave` flow applies: set `autoLeave.onBotDetected: true` to have the bot leave on competing-notetaker detection. ## Known quirks - Some Jitsi builds skip the prejoin pane entirely and drop the bot straight into the conference. The adapter detects this via `APP.conference.isJoined()` and proceeds without filling a name — the display name is set later via `APP.conference.changeLocalDisplayName()`. - Self-hosted Jitsi deployments often have heavily customized branding, but they keep the lib-jitsi-meet API stable. If you see `jitsi:not admitted` on a self-hosted deployment, file an issue with the conference object's shape (run `Object.keys(window.APP.conference)` in DevTools). ## Failure codes | Jitsi sentinel | `failure_code` | | -------------------------------------------- | ----------------------- | | "Password-protected" / requires password | `auth_failure` | | "Conference connection failed" (bridge down) | `network_loss` | | "Moderator denied your request" | `host_denied_admission` | | "You have been kicked" | `removed_from_meeting` | | Stuck in lobby past `afterEntryDelaySeconds` | `lobby_timeout` | --- ## Cisco Webex Source: https://meetbot.dev/docs/per-platform/webex > Joining Cisco Webex Meetings as a meetbot — supported URL patterns, lobby behavior, captions, known quirks. The `webex` adapter joins Cisco Webex Meetings via browser automation on the web client (`*.webex.com/...`). No Webex SDK, no marketplace app, no JWT signing — anonymous guest join just like Meet/Teams. ## Supported URL patterns | Pattern | Notes | | ---------------------------------------------------- | ------------------------------------ | | `https://*.webex.com/meet/` | Personal Rooms | | `https://*.webex.com/wbxmjrn/sites/.../` | Scheduled meetings (legacy URL form) | | `https://*.webex.com/webappng/sites/.../meeting/...` | Web-app NG meeting URLs | The platform discriminator is auto-detected by host suffix (`*.webex.com`); you can override it explicitly via `platform: "webex"` on dispatch if you need to force the routing. ```bash curl -X POST https://api.meetbot.dev/api/v1/jobs \ -H "authorization: Bearer $MEETBOT_API_KEY" \ -H "content-type: application/json" \ -d '{ "externalId": "webex-demo-2026-05-09", "meetingUrl": "https://meetingsapac1.webex.com/meet/alice", "platform": "webex", "displayName": "meetbot", "webhooks": {"onFinalize": "https://yours.example/hook/meetbot"} }' ``` ## Lobby behavior - **Personal Rooms** with default settings auto-admit guests; the bot goes from `joining` → `in_meeting` without an `entering_lobby` stop. - **Enterprise tenants** that enable "Allow guests" off OR "Lobby for guests" require host approval — the bot reports `entering_lobby` and waits for admission. Configure `autoLeave.afterEntryDelaySeconds` to bound the wait (default 600s / 10 min). - **SSO-required meetings** can't be joined by an anonymous bot. The job fails with `failure_code: auth_failure` and the `failure_detail` carries the sentinel that triggered detection ("SSO authentication is required"). Migrate the meeting to the guest-join policy or supply OAuth credentials (M2 ticket). ## Captions The adapter toggles closed captions via the toolbar's More menu when the tenant has captions enabled in the meeting policy. When captions are off-by-policy the toggle silently no-ops; transcription falls back to the bot's own audio capture (per-speaker WebRTC tracks → Whisper or your STT of choice). ## Roster + competing-bot detection The bot scrapes the participants panel for display names and feeds them into the shared `evaluateAutoLeave` evaluator. The same auto-leave-on-bot-detected behavior available for Meet/Teams/Zoom is available for Webex — set `autoLeave.onBotDetected: true` on dispatch. ## Known quirks - Webex's "Start meeting" button (host view) is rendered as the same CTA as the guest "Join meeting" button — the bot will never see the host CTA because we always join as a guest. No special handling required. - Some tenants serve a "Download the desktop app" interstitial before the join page; the adapter dismisses it by clicking "Join from your browser" / "Use web app" / "Continue in browser" whichever variant is rendered. - Captions DOM contract has shifted across Webex builds. The adapter has fallbacks but if you observe `webex:captions-not-enabled` on a tenant that does support captions, file an issue with the `data-testid` attributes on the captions toggle. ## Failure codes The adapter maps Webex-specific failure modes to the generic `failure_code` taxonomy: | Webex sentinel | `failure_code` | | -------------------------------------------- | ---------------------- | | "SSO authentication is required" | `auth_failure` | | "This meeting has ended" | `meeting_ended` | | "Meeting number invalid / expired" | `meeting_not_found` | | Stuck in lobby past `afterEntryDelaySeconds` | `lobby_timeout` | | Bot removed from meeting mid-recording | `removed_from_meeting` | --- ## Whereby Source: https://meetbot.dev/docs/per-platform/whereby > Joining Whereby rooms as a meetbot — supported URL patterns, knock-to-enter behavior, integration notes. The `whereby` adapter joins Whereby rooms via browser automation on the public web client (`whereby.com/` and subdomains). Whereby is WebRTC-native with no anti-bot fingerprint surface, so the integration is significantly cleaner than Meet/Teams/Zoom. ## Supported URL patterns | Pattern | Notes | | ------------------------------------- | ----------------------------------- | | `https://whereby.com/` | Public room | | `https://.whereby.com/` | Hosted-subdomain (Whereby Embedded) | The platform discriminator is auto-detected by host; pass `platform: "whereby"` explicitly if you want to bypass detection. ```bash curl -X POST https://api.meetbot.dev/api/v1/jobs \ -H "authorization: Bearer $MEETBOT_API_KEY" \ -H "content-type: application/json" \ -d '{ "externalId": "whereby-demo-2026-05-09", "meetingUrl": "https://whereby.com/teamname-room", "platform": "whereby", "displayName": "meetbot", "webhooks": {"onFinalize": "https://yours.example/hook/meetbot"} }' ``` ## Lobby behavior - **Personal rooms** auto-admit; the bot lands directly in the meeting. - **Knock-to-enter rooms** show a "Waiting for someone to let you in" screen; the host has to accept. The bot stays in `entering_lobby` until admitted or `afterEntryDelaySeconds` expires (default 600s / 10 min). - **Locked rooms** trip a retryable failure — the host can unlock and the bot's existing retry budget (3 hard attempts) will re-knock. ## Captions Whereby has no native captions toggle today (the platform relies on third-party transcription integrations). The adapter logs `whereby:captions-skipped` and audio capture continues normally — your downstream STT pipeline (Whisper, Deepgram, etc) processes the per-speaker WebRTC tracks the bot ships. ## Roster + competing-bot detection Roster is scraped from the participant tiles (`[data-testid^="participant-"]`); the same `evaluateAutoLeave` shared across all platforms applies. Set `autoLeave.onBotDetected: true` to have the bot leave when a competing notetaker (Otter, Fireflies, etc) is observed in the room. ## Known quirks - Whereby Embedded customers (white-labeled tenants) keep the same DOM contract as `whereby.com` — the adapter works without modification. - The bot's join is reliable enough that we recommend it as the default test platform for new meetbot deployments — easier signal-to-noise than fighting Meet's anti-bot stack. ## Failure codes | Whereby sentinel | `failure_code` | | -------------------------------------------- | ----------------------- | | "That room does not exist" | `meeting_not_found` | | "The meeting has ended" | `meeting_ended` | | "This room is locked" (after retry budget) | `host_denied_admission` | | Stuck in knock past `afterEntryDelaySeconds` | `lobby_timeout` | --- ## Go SDK (shipping Q3 2026) Source: https://meetbot.dev/docs/sdks/go > Official meetbot SDK for Go. Hand-written ergonomic facade over an oapi-codegen-generated transport. Importable as github.com/meetbot-dev/meetbot/sdk-go. The Go SDK is on the public roadmap for **Q3 2026**. While we ship it, you have two viable paths: 1. **Use the OpenAPI spec directly** with `oapi-codegen`: ```bash go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest curl https://meetbot.dev/openapi.yaml -o meetbot.yaml oapi-codegen -package meetbot meetbot.yaml > meetbot/client.go ``` This gives you typed structs + a low-level client that mirrors every endpoint. We do exactly this internally — the official SDK adds an ergonomic facade on top. 2. **Use the [CLI](/docs/ai-tools/cli) via `os/exec`** for one-shot ops. Single static binary, zero Go deps, JSON output via `--json` pipes cleanly into your code. ## Planned API shape When the official Go SDK lands it will look like this — copy this into your design docs as the authoritative future surface: ```go package main import ( "context" "fmt" "os" "github.com/meetbot-dev/meetbot/sdk-go/meetbot" ) func main() { mb, err := meetbot.NewClient(meetbot.Config{ APIKey: os.Getenv("MEETBOT_API_KEY"), }) if err != nil { panic(err) } job, err := mb.DispatchBot(context.Background(), meetbot.DispatchBotInput{ ExternalID: "lesson-2026-05-09-pavel", MeetingURL: "https://meet.google.com/abc-defg-hij", DisplayName: "Acme Notetaker", }) if err != nil { panic(err) } fmt.Printf("dispatched %s → %s\n", job.ID, job.Status) } ``` Webhook verification: ```go import "github.com/meetbot-dev/meetbot/sdk-go/meetbot" func handler(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) if !meetbot.VerifyWebhookSignature( raw, r.Header.Get("X-Meetbot-Signature"), os.Getenv("MEETBOT_WEBHOOK_SECRET"), ) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // parse + handle } ``` ## Distribution - Module path: `github.com/meetbot-dev/meetbot/sdk-go` - Versioned via Go module subdirectory tags (`sdk-go/v0.1.0`, etc.) - Published via `git tag` push; `go get` resolves through the default GOPROXY automatically. - Min Go version: **1.22** (decided at ship time; locked here for customer planning). ## Status 🚧 **In design.** The package skeleton lives at `packages/sdk-go/` in the monorepo. Track the milestone at [github.com/meetbot-dev/meetbot/milestones](https://github.com/meetbot-dev/meetbot/milestones). For build philosophy and the codegen-vs-handwritten tradeoffs, see [`docs/sdk-shipping-strategy.md`](https://github.com/meetbot-dev/meetbot/blob/main/docs/sdk-shipping-strategy.md). ## See also - [TypeScript SDK](/docs/sdks/typescript) — same surface today - [Python SDK](/docs/sdks/python) — same surface today - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) — Go snippet included --- ## Python SDK Source: https://meetbot.dev/docs/sdks/python > Official meetbot-sdk on PyPI. Sync + async clients, pydantic v2 models, webhook signature verification. Python 3.10+. The official Python SDK ships as `meetbot-sdk` on PyPI (the bare `meetbot` name was already taken by an unrelated 2024 project; the Python **import** is `meetbot`). Source is MIT at [github.com/meetbot-dev/meetbot/tree/main/packages/sdk-python](https://github.com/meetbot-dev/meetbot/tree/main/packages/sdk-python). ## Install ```bash pip install meetbot-sdk ``` ```bash uv add meetbot-sdk ``` ```bash poetry add meetbot-sdk ``` Requires Python 3.10+. Sync (`MeetBot`) and async (`AsyncMeetBot`) clients ship in the same package; HTTP transport is `httpx`. ## Auth ```python from meetbot import MeetBot mb = MeetBot(api_key="mb_live_…") # explicit mb = MeetBot() # picks up $MEETBOT_API_KEY ``` | env var | default | purpose | | ---------------------- | ------------------------- | ------------------------------------- | | `MEETBOT_API_KEY` | — | Bearer token. Required if not passed. | | `MEETBOT_API_BASE_URL` | `https://api.meetbot.dev` | Override for self-hosted / staging. | ## Method reference All methods on `MeetBot` have an identical async twin on `AsyncMeetBot` (same name, same kwargs, same return type, plus `await`). | Method | HTTP | Returns | | ---------------------------------------- | ----------------------------------------------------- | ---------------------------------- | | `dispatch_bot(...)` | `POST /api/v1/jobs` | `Job` | | `get_bot(job_id)` | `GET /api/v1/jobs/{id}` | `Job` or `None` (404 → `None`) | | `list_bots(...)` | _M1.1_ | `list[Job]` | | `cancel_bot(job_id)` | `DELETE /api/v1/jobs/{id}` | `None` | | `request_recording_permission(job_id)` | `POST /api/v1/jobs/{id}/request-recording-permission` | `RequestRecordingPermissionResult` | | `pause_recording(job_id)` | `POST /api/v1/jobs/{id}/pause-recording` | `RequestRecordingPermissionResult` | | `resume_recording(job_id)` | `POST /api/v1/jobs/{id}/resume-recording` | `RequestRecordingPermissionResult` | | `stop_recording(job_id)` | `POST /api/v1/jobs/{id}/stop-recording` | `RequestRecordingPermissionResult` | | `leave_bot(job_id)` | `POST /api/v1/jobs/{id}/leave-call` | `RequestRecordingPermissionResult` | | `send_chat_message(job_id, message)` | `POST /api/v1/jobs/{id}/send-chat-message` | `RequestRecordingPermissionResult` | | `verify_webhook_signature(...)` (module) | n/a — local | `bool` | ## Example 1 — dispatch + poll until completion ```python import os import time from meetbot import MeetBot with MeetBot(api_key=os.environ["MEETBOT_API_KEY"]) as mb: job = mb.dispatch_bot( external_id="lesson-2026-05-09-pavel", meeting_url="https://meet.google.com/abc-defg-hij", display_name="Acme Notetaker", ) print(f"dispatched {job.id} → {job.status}") while True: snapshot = mb.get_bot(job.id) assert snapshot is not None print(snapshot.status) if snapshot.status in ("completed", "failed", "cancelled"): print("manifest:", snapshot.manifest_uri) break time.sleep(5) ``` ## Example 2 — async dispatch with auto-leave + metadata ```python import asyncio from datetime import datetime, timezone from meetbot import AsyncMeetBot, AutoLeaveConfig async def main() -> None: async with AsyncMeetBot() as mb: job = await mb.dispatch_bot( external_id="q2-board-meeting-acme", meeting_url="https://meet.google.com/xyz-abcd-efg", join_at=datetime(2026, 5, 15, 14, 0, tzinfo=timezone.utc), display_name="Acme Notetaker", on_finalize_url="https://api.acme.example/meetbot/finalize", metadata={"customer_id": "cust_4711"}, auto_leave=AutoLeaveConfig( after_entry_delay_seconds=300, after_max_seconds=7200, on_bot_detected=True, ), ) print(job.id, job.status, job.join_at) asyncio.run(main()) ``` ## Example 3 — verify an inbound webhook (Flask) The signed webhook is the recommended completion signal. Verify **before** parsing — anyone with your webhook URL could otherwise POST garbage at it. ```python import os from flask import Flask, request, abort from meetbot import verify_webhook_signature app = Flask(__name__) WEBHOOK_SECRET = os.environ["MEETBOT_WEBHOOK_SECRET"] # whsec_… @app.post("/webhook/meetbot") def meetbot_hook(): raw = request.get_data() # bytes — do NOT use request.json if not verify_webhook_signature( body=raw, header=request.headers.get("X-Meetbot-Signature"), secret=WEBHOOK_SECRET, ): abort(401) event = request.get_json() if event["data"]["event"] == "job.finalized": manifest_uri = event["data"]["manifestUri"] process_recording_async(event["data"]["jobId"], manifest_uri) return "ok" ``` The same algorithm in JavaScript and Go is at [Webhooks: Signature Verification](/docs/basics/webhooks-verify). ## Errors Every SDK exception subclasses `MeetbotError`. HTTP errors carry both a numeric `status` and the orchestrator's machine-readable `code`: ```python from meetbot import ( MeetBot, MeetbotNotFoundError, MeetbotRateLimitError, MeetbotUnauthorizedError, MeetbotValidationError, ) try: job = mb.dispatch_bot(external_id="bad", meeting_url="not-a-url") except MeetbotValidationError as e: print("validation:", e.body) # {"error": "invalid_input", "issues": [...]} except MeetbotRateLimitError as e: print("over quota / rate-limited:", e.status, e.code) except MeetbotUnauthorizedError: print("rotate the key at /account/keys") ``` ## Retry + idempotency - **Transport-level retry:** 3x with exponential backoff + jitter on HTTP 429 / 500 / 502 / 503 / 504 and `httpx.TransportError`. Honours `Retry-After`. Override via `MeetBot(max_retries=…)`. - **Idempotency:** every POST sends an auto-generated `Idempotency-Key` header. Pass `idempotency_key=` to fix it yourself. - **Server-side dedupe:** `dispatch_bot()`'s `external_id` doubles as a server-side idempotency key — reposting the same `external_id` returns the existing job. ## See also - [Bot Quickstart](/docs/meeting-bots/quickstart) - [TypeScript SDK](/docs/sdks/typescript) — same surface in TS - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) - [Failure sub-codes](/docs/meeting-bots/sub-codes) --- ## Rust SDK (shipping Q4 2026) Source: https://meetbot.dev/docs/sdks/rust > Official meetbot crate. Async-first via tokio + reqwest, hand-written ergonomic facade over a progenitor-generated transport. The Rust SDK is on the public roadmap for **Q4 2026**, after the Python (M6) and Go (Q3) SDKs land. While we ship it, you have two viable paths: 1. **Use the OpenAPI spec directly** with [`progenitor`](https://github.com/oxidecomputer/progenitor): ```toml [build-dependencies] progenitor = "0.7" ``` ```rust // build.rs let spec = std::fs::read_to_string("meetbot.yaml")?; let mut generator = progenitor::Generator::default(); let tokens = generator.generate_tokens(&serde_yaml::from_str(&spec)?)?; std::fs::write("src/generated.rs", prettyplease::unparse(&tokens.into()))?; ``` 2. **Use the [CLI](/docs/ai-tools/cli) via `std::process::Command`** for one-shot ops. ## Planned API shape When the official Rust SDK lands, the surface will look like: ```rust use meetbot::{Client, DispatchBotInput}; #[tokio::main] async fn main() -> Result<(), meetbot::Error> { let mb = Client::builder() .api_key(std::env::var("MEETBOT_API_KEY")?) .build()?; let job = mb.dispatch_bot(DispatchBotInput { external_id: "lesson-2026-05-09-pavel".into(), meeting_url: "https://meet.google.com/abc-defg-hij".into(), display_name: Some("Acme Notetaker".into()), ..Default::default() }).await?; println!("dispatched {} → {:?}", job.id, job.status); Ok(()) } ``` Webhook verification: ```rust use meetbot::verify_webhook_signature; async fn handler(headers: HeaderMap, body: Bytes) -> StatusCode { let header = headers.get("x-meetbot-signature").and_then(|h| h.to_str().ok()); let secret = std::env::var("MEETBOT_WEBHOOK_SECRET").unwrap(); if !verify_webhook_signature(&body, header, &secret) { return StatusCode::UNAUTHORIZED; } // parse + handle StatusCode::OK } ``` ## Distribution - crates.io name: `meetbot` (we'll reserve before Q4) - MSRV (minimum supported Rust version): **1.78**, locked at ship. - Async-first via `tokio` + `reqwest`. Sync facade via `tokio::runtime::Runtime::block_on` if customer demand emerges. - `serde` + `serde_json` for models; no `pydantic`-style runtime validation — Rust's type system does that statically. ## Status 🚧 **In design.** The crate skeleton lives at `packages/sdk-rust/` in the monorepo. Tracked in [github.com/meetbot-dev/meetbot/milestones](https://github.com/meetbot-dev/meetbot/milestones). For build philosophy + codegen-vs-handwritten tradeoffs, see [`docs/sdk-shipping-strategy.md`](https://github.com/meetbot-dev/meetbot/blob/main/docs/sdk-shipping-strategy.md). ## See also - [TypeScript SDK](/docs/sdks/typescript) - [Python SDK](/docs/sdks/python) - [Go SDK](/docs/sdks/go) — ships Q3 2026 --- ## TypeScript / JavaScript SDK Source: https://meetbot.dev/docs/sdks/typescript > Official @meetbot/sdk for Node.js 22+. Typed HTTP client, webhook signature verification, full surface for the meetbot orchestrator API. The official TypeScript SDK is `@meetbot/sdk` on npm. Source is MIT at [github.com/meetbot-dev/meetbot/tree/main/packages/sdk](https://github.com/meetbot-dev/meetbot/tree/main/packages/sdk). ## Install ```bash npm install @meetbot/sdk ``` ```bash pnpm add @meetbot/sdk ``` ```bash yarn add @meetbot/sdk ``` ```bash bun add @meetbot/sdk ``` Requires Node 22+. ESM-only (no CommonJS shim — Node 22 ships native ESM). ## Auth ```ts import { createMeetbot } from "@meetbot/sdk"; const meetbot = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY!, // mb_live_… or mb_test_… baseUrl: "https://api.meetbot.dev", // optional; this is the default }); ``` Get a key at [meetbot.dev/account/keys](https://meetbot.dev/account/keys). ## Method reference | Method | HTTP | Returns | | -------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------- | | `meetbot.dispatchBot(input)` | `POST /api/v1/jobs` | `Promise` | | `meetbot.getJob(id)` | `GET /api/v1/jobs/{id}` | `Promise` | | `meetbot.cancelJob(id)` | `DELETE /api/v1/jobs/{id}` | `Promise` | | `meetbot.requestRecordingPermission(id)` | `POST /api/v1/jobs/{id}/request-recording-permission` | `Promise` | | `verifyWebhookSignature({ body, header, secret })` | n/a — local | `boolean` | Bot-control verbs (`pause_recording`, `resume_recording`, `stop_recording`, `leave_call`, `send_chat_message`, `pin_participant`) are wired into the SDK in M1.1. ## Example 1 — dispatch a bot ```ts import { createMeetbot } from "@meetbot/sdk"; const meetbot = createMeetbot({ apiKey: process.env.MEETBOT_API_KEY! }); const job = await meetbot.dispatchBot({ externalId: "lesson-2026-05-09-pavel", meetingUrl: "https://meet.google.com/abc-defg-hij", displayName: "Acme Notetaker", webhooks: { onFinalize: "https://api.acme.example/meetbot/finalize", }, }); console.log("dispatched", job.id, "→", job.status); ``` ## Example 2 — verify a webhook ```ts title="app/api/webhook/meetbot/route.ts" import { verifyWebhookSignature } from "@meetbot/sdk"; export async function POST(req: Request) { const raw = await req.text(); const ok = verifyWebhookSignature({ body: raw, header: req.headers.get("x-meetbot-signature"), secret: process.env.MEETBOT_WEBHOOK_SECRET!, }); if (!ok) return new Response("invalid signature", { status: 401 }); const event = JSON.parse(raw); // …handle event.data.event return new Response("ok"); } ``` The full algorithm and gotchas are at [Webhooks: Signature Verification](/docs/basics/webhooks-verify). ## Example 3 — auto-leave + custom metadata ```ts const job = await meetbot.dispatchBot({ externalId: "q2-board-meeting-acme", meetingUrl: "https://meet.google.com/xyz-abcd-efg", joinAt: new Date("2026-05-15T14:00:00Z"), metadata: { customer_id: "cust_4711", workspace: "acme-prod" }, autoLeave: { afterEntryDelaySeconds: 300, // leave if not let in within 5m afterMaxSeconds: 7200, // cap at 2h onBotDetected: true, // leave if Otter/Fireflies/Read shows up }, }); ``` ## Errors `MeetbotHttpError` carries a numeric `status` and an optional machine- readable `code` from the orchestrator's error envelope: ```ts import type { MeetbotHttpError } from "@meetbot/sdk"; try { await meetbot.dispatchBot({ externalId: "bad", meetingUrl: "not-a-url" }); } catch (err) { const e = err as MeetbotHttpError; if (e.status === 400) console.error("validation:", e); if (e.status === 401) console.error("auth:", e); if (e.status === 429) console.error("rate limit:", e); throw err; } ``` ## See also - [Bot Quickstart](/docs/meeting-bots/quickstart) — end-to-end walkthrough - [Failure sub-codes](/docs/meeting-bots/sub-codes) — `failureCode` taxonomy - [Webhooks: Signature Verification](/docs/basics/webhooks-verify) - [Python SDK](/docs/sdks/python) — same surface in Python