> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# Official SDKs

> Install @meetstream/sdk or meetstream-sdk to create meeting bots, fetch transcripts, run MIA voice agents, and verify webhooks with a typed client that handles the API's polling and idempotency semantics for you.

MeetStream ships official clients for TypeScript and Python. Both cover the full API surface, both are typed, and both encode the handful of API behaviours that are easy to get wrong.

#### TypeScript

```bash
npm install @meetstream/sdk
```

Node 18.17+. ESM and CommonJS. **Zero runtime dependencies.**

```ts
import { MeetStream } from '@meetstream/sdk';

const meetstream = new MeetStream(); // reads MEETSTREAM_API_KEY

const bot = await meetstream.bots.create({
  meeting_link: 'https://meet.google.com/abc-defg-hij',
  bot_name: 'Notetaker',
  recording_config: {
    transcript: { provider: { deepgram: { model: 'nova-3', language: 'en' } } },
  },
});

const transcript = await meetstream.transcripts.waitFor(bot.transcript_id!);
```

#### Python

```bash
pip install meetstream-sdk
```

Python 3.8+. Sync and async. Ships `py.typed`.

```python
from meetstream import MeetStream

meetstream = MeetStream()  # reads MEETSTREAM_API_KEY

bot = meetstream.bots.create({
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "bot_name": "Notetaker",
    "recording_config": {
        "transcript": {"provider": {"deepgram": {"model": "nova-3", "language": "en"}}}
    },
})

transcript = meetstream.transcripts.wait_for(bot["transcript_id"])
```

The distribution is `meetstream-sdk`; the import name is `meetstream`.

Get an API key from the [MeetStream dashboard](https://app.meetstream.ai/api-key).

The SDKs talk to the REST API and send `Authorization: Token <key>`. The [MCP server](/build-with-ai/meetstream-mcp-server) uses `Bearer` instead. The two schemes are not interchangeable, and the SDKs handle this for you.

## Async (Python)

```python
from meetstream import AsyncMeetStream

async with AsyncMeetStream() as meetstream:
    bot = await meetstream.bots.create({"meeting_link": "https://zoom.us/j/123"})
    status = await meetstream.bots.status(bot["bot_id"])
```

Every synchronous method has an awaitable twin with the same name and signature.

## What the SDKs handle for you

A few API behaviours look like bugs until you know them. Rather than documenting them and hoping, the SDKs encode them.

| Behaviour                                                                                          | What the SDK does                                                             |
| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **`202` sits inside the 2xx range** and means "still processing", not success                      | Raises `NotReadyError` rather than handing back an empty body                 |
| **`507` looks like a failure** but is an idempotent replay                                         | Resolves as success, returning the original bot                               |
| **Streaming-only providers return `202` forever**, because no post-call transcript will ever exist | `waitFor` / `wait_for` is always bounded and tells you exactly why it gave up |
| **Transcripts are keyed by `transcript_id`**, not `bot_id`                                         | The transcript methods take the right identifier                              |
| **Segments use a `transcript` field**, not `text`                                                  | Typed in TypeScript, documented at each call site in Python                   |
| **`remove_bot` is a `GET`**                                                                        | `bots.remove()` issues the right verb                                         |
| **`in_call_recording_timeout` has a 600 second floor**                                             | Documented on the type, so a 400 is avoidable before you send it              |
| **MIA attaches with only `agent_config_id`**                                                       | Extra bridge fields are the usual cause of a silent agent                     |

Retries are built in for `429` and `5xx` with exponential backoff, honouring `Retry-After`.

## Coverage

Seven resource namespaces spanning every endpoint in the [API reference](/api-reference/introduction).

#### Bots: lifecycle, media, live interaction

#### TypeScript

```ts
await meetstream.bots.create(params, { idempotencyKey: uuid });
await meetstream.bots.list();
await meetstream.bots.status(botId);
await meetstream.bots.detail(botId);       // includes transcript_id
await meetstream.bots.summary(botId);      // AI summary
await meetstream.bots.remove(botId);       // leave, keep the data
await meetstream.bots.deleteData(botId);   // irreversible

await meetstream.bots.audio(botId);
await meetstream.bots.video(botId);
await meetstream.bots.audioStreams(botId);      // per-participant audio
await meetstream.bots.recordingStreams(botId);  // per-participant video
await meetstream.bots.screenshots(botId);
await meetstream.bots.waitForAudio(botId);      // bounded

await meetstream.bots.participants(botId);
await meetstream.bots.chats(botId);
await meetstream.bots.speakerTimeline(botId);

await meetstream.bots.sendMessage(botId, 'Recording has started.');
await meetstream.bots.sendImage(botId, { img_url: 'https://example.com/slide.png' });
await meetstream.bots.pauseRecording(botId);
await meetstream.bots.resumeRecording(botId);
```

#### Python

```python
meetstream.bots.create(params, idempotency_key=uuid)
meetstream.bots.list()
meetstream.bots.status(bot_id)
meetstream.bots.detail(bot_id)        # includes transcript_id
meetstream.bots.summary(bot_id)       # AI summary
meetstream.bots.remove(bot_id)        # leave, keep the data
meetstream.bots.delete_data(bot_id)   # irreversible

meetstream.bots.audio(bot_id)
meetstream.bots.video(bot_id)
meetstream.bots.audio_streams(bot_id)       # per-participant audio
meetstream.bots.recording_streams(bot_id)   # per-participant video
meetstream.bots.screenshots(bot_id)
meetstream.bots.wait_for_audio(bot_id)      # bounded

meetstream.bots.participants(bot_id)
meetstream.bots.chats(bot_id)
meetstream.bots.speaker_timeline(bot_id)

meetstream.bots.send_message(bot_id, "Recording has started.")
meetstream.bots.send_image(bot_id, "https://example.com/slide.png")
meetstream.bots.pause_recording(bot_id)
meetstream.bots.resume_recording(bot_id)
```

#### Transcripts

#### TypeScript

```ts
await meetstream.transcripts.get(transcriptId);
await meetstream.transcripts.waitFor(transcriptId, { timeoutMs: 900_000 });
await meetstream.transcripts.listForBot(botId);
await meetstream.transcripts.transcribeBotAudio(botId);
```

#### Python

```python
meetstream.transcripts.get(transcript_id)
meetstream.transcripts.wait_for(transcript_id, timeout=900)
meetstream.transcripts.list_for_bot(bot_id)
meetstream.transcripts.transcribe_bot_audio(bot_id)
```

`transcribeBotAudio` / `transcribe_bot_audio` is the rescue path when a bot used a [streaming-only provider](/guides/transcription-recordings/live-transcription): the live stream was the only record, and this generates a post-call transcript from the stored audio afterwards.

#### Calendar: auto-join and scheduling

#### TypeScript

```ts
await meetstream.calendar.connectGoogle({ google_client_id, google_client_secret, google_refresh_token });
await meetstream.calendar.connectOutlook({ /* credentials */ });
await meetstream.calendar.events();
await meetstream.calendar.scheduleEvent(eventId);
await meetstream.calendar.listScheduledBots();
await meetstream.calendar.rescheduleBot(botId, { scheduled_join_time: '2026-09-01T10:00:00Z' });
await meetstream.calendar.enableAutoSchedule();
```

#### Python

```python
meetstream.calendar.connect_google({...})
meetstream.calendar.connect_outlook({...})
meetstream.calendar.events()
meetstream.calendar.schedule_event(event_id)
meetstream.calendar.list_scheduled_bots()
meetstream.calendar.reschedule_bot(bot_id, "2026-09-01T10:00:00Z")
meetstream.calendar.enable_auto_schedule()
```

Setup guides: [Google Calendar](/guides/calendar-integrations/google-calendar-oauth-setup) and [Outlook](/guides/calendar-integrations/outlook-calendar-setup).

#### MIA voice agents

#### TypeScript

```ts
const agent = await meetstream.mia.create({
  agent_name: 'Meeting Assistant',
  mode: 'pipeline',
  model:       { provider: 'openai', model: 'gpt-4.1', first_message: 'Hi, I am an AI assistant on this call.' },
  voice:       { provider: 'openai', voice_id: 'nova' },
  transcriber: { provider: 'deepgram', model: 'nova-3', boostwords: ['Acme'] },
  agent:       { tools: [], mcp_servers: [], enable_interruptions: true },
  wake_word:   { enabled: true, words: ['hey acme'], timeout: 30 },
});

await meetstream.bots.create({ meeting_link, agent_config_id: agent.agent_config_id });
```

#### Python

```python
agent = meetstream.mia.create({
    "agent_name": "Meeting Assistant",
    "mode": "pipeline",
    "model": {"provider": "openai", "model": "gpt-4.1",
              "first_message": "Hi, I am an AI assistant on this call."},
    "voice": {"provider": "openai", "voice_id": "nova"},
    "transcriber": {"provider": "deepgram", "model": "nova-3", "boostwords": ["Acme"]},
    "agent": {"tools": [], "mcp_servers": [], "enable_interruptions": True},
    "wake_word": {"enabled": True, "words": ["hey acme"], "timeout": 30},
})

meetstream.bots.create({
    "meeting_link": meeting_link,
    "agent_config_id": agent["agent_config_id"],
})
```

Attach an agent with **`agent_config_id` alone**. Passing `socket_connection_url` or `live_audio_required` alongside it is the most common cause of an agent that joins but never speaks, because those fields are for bring-your-own-bridge setups and MeetStream hosts the MIA bridge itself.

See [Create MIA](/guides/mia/create-mia) and [MIA configurations](/guides/mia/mia-configurations).

#### Google signed-in bots, Zoom OAuth, and custom storage

#### TypeScript

```ts
await meetstream.googleLogins.createDomain({ /* domain config */ });
await meetstream.googleLogins.create({ /* login */ });

await meetstream.zoom.authorizeUrl();
await meetstream.zoom.listConnections();

await meetstream.storage.set({ provider: 'aws', bucket_name, region, access_key_id, secret_key });
```

#### Python

```python
meetstream.google_logins.create_domain({...})
meetstream.google_logins.create({...})

meetstream.zoom.authorize_url()
meetstream.zoom.list_connections()

meetstream.storage.set({"provider": "aws", "bucket_name": ..., "region": ...})
```

Guides: [Google signed-in bots](/guides/app-integrations/google-signed-in-bots), [Zoom OBF](/guides/app-integrations/zoom-obf-implementation), [custom storage](/guides/features/custom-storage-configurations).

## Webhooks

Both SDKs verify signatures. Pass the **raw** request body: re-serializing a parsed object changes key order and whitespace, so the signature will never match.

#### TypeScript

```ts
import express from 'express';
import { parseWebhook, isTerminal, describeStop } from '@meetstream/sdk';

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = parseWebhook(req.body, req.header('x-meetstream-signature')!, process.env.WEBHOOK_SECRET!);
  } catch {
    return res.sendStatus(401);
  }

  if (isTerminal(event)) console.log(describeStop(event));
  res.sendStatus(200);
});
```

Use `express.raw()` on the webhook route, not `express.json()`.

#### Python

```python
from flask import Flask, request
from meetstream import parse_webhook, is_terminal, describe_stop

@app.post("/webhook")
def webhook():
    try:
        event = parse_webhook(
            request.get_data(),
            request.headers.get("X-MeetStream-Signature", ""),
            os.environ["WEBHOOK_SECRET"],
        )
    except ValueError:
        return "", 401

    if is_terminal(event):
        print(describe_stop(event))
    return "", 200
```

Use `request.get_data()` for the raw bytes, not `request.json`.

`isTerminal` / `is_terminal` recognises `bot.stopped`, which is the single terminal event and always carries `status_code: 200`. The reason lives in `bot_status`, and `describeStop` / `describe_stop` turns it into a sentence. Full lifecycle: [Webhooks and events](/guides/webhooks/webhooks-and-events).

## Errors

Every failure maps to a distinct class carrying the API's own `message`, the HTTP `status`, and the request id.

#### TypeScript

```ts
import { NotReadyError, RateLimitError, BadRequestError, MeetStreamError } from '@meetstream/sdk';

try {
  await meetstream.transcripts.get(id);
} catch (err) {
  if (err instanceof NotReadyError) { /* 202, poll again */ }
  else if (err instanceof RateLimitError) { await sleep(err.retryAfter! * 1000); }
  else if (err instanceof BadRequestError) { console.error(err.message); }
  else if (err instanceof MeetStreamError) { console.error(err.status, err.requestId); }
}
```

#### Python

```python
from meetstream import NotReadyError, RateLimitError, BadRequestError, MeetStreamError

try:
    meetstream.transcripts.get(transcript_id)
except NotReadyError:
    ...                                  # 202, poll again
except RateLimitError as e:
    time.sleep(e.retry_after or 5)
except BadRequestError as e:
    print(e.message)
except MeetStreamError as e:
    print(e.status, e.request_id)
```

`AuthenticationError` (401) means the header never arrived. `PermissionError` (403) means the key itself was rejected. Full list: [Errors](/errors).

## Configuration

#### TypeScript

```ts
const meetstream = new MeetStream({
  apiKey: process.env.MEETSTREAM_API_KEY,
  baseUrl: 'https://api.meetstream.ai/api/v1',
  timeout: 60_000,
  maxRetries: 2,
  fetch: customFetch,
});
```

#### Python

```python
meetstream = MeetStream(
    api_key=os.environ["MEETSTREAM_API_KEY"],
    base_url="https://api.meetstream.ai/api/v1",
    timeout=60.0,
    max_retries=2,
    http_client=my_httpx_client,
)
```

| Variable             | Purpose                                     |
| -------------------- | ------------------------------------------- |
| `MEETSTREAM_API_KEY` | Your key. Required unless passed explicitly |
| `MEETSTREAM_API_URL` | Override the API base, for staging          |

Anything the SDK does not wrap yet is reachable through the raw transport, so a new endpoint never blocks you:

#### TypeScript

```ts
await meetstream.http.get('/some/new/endpoint');
```

#### Python

```python
meetstream.http.get("/some/new/endpoint")
```

## Source and support

#### [TypeScript SDK](https://www.npmjs.com/package/@meetstream/sdk)

`@meetstream/sdk` on npm. Source at [meetstream-node](https://github.com/meetstream-ai/meetstream-node).

#### [Python SDK](https://pypi.org/project/meetstream-sdk/)

`meetstream-sdk` on PyPI. Source at [meetstream-python](https://github.com/meetstream-ai/meetstream-python).

Prefer a different surface? The [MCP server](/build-with-ai/meetstream-mcp-server) exposes the same capability as tools for coding agents, and the [CLI](/build-with-ai/meetstream-cli) covers it from your terminal. Runnable examples live in [Labs](https://github.com/meetstream-ai/labs).