# 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