Official SDKs

Typed MeetStream clients for TypeScript and Python
View as Markdown

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.

npm install @meetstream/sdk

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

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!);

Get an API key from the MeetStream dashboard.

The SDKs talk to the REST API and send Authorization: Token <key>. The MCP server uses Bearer instead. The two schemes are not interchangeable, and the SDKs handle this for you.

Async (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.

BehaviourWhat the SDK does
202 sits inside the 2xx range and means “still processing”, not successRaises NotReadyError rather than handing back an empty body
507 looks like a failure but is an idempotent replayResolves as success, returning the original bot
Streaming-only providers return 202 forever, because no post-call transcript will ever existwaitFor / wait_for is always bounded and tells you exactly why it gave up
Transcripts are keyed by transcript_id, not bot_idThe transcript methods take the right identifier
Segments use a transcript field, not textTyped in TypeScript, documented at each call site in Python
remove_bot is a GETbots.remove() issues the right verb
in_call_recording_timeout has a 600 second floorDocumented on the type, so a 400 is avoidable before you send it
MIA attaches with only agent_config_idExtra 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.

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);
await meetstream.transcripts.get(transcriptId);
await meetstream.transcripts.waitFor(transcriptId, { timeoutMs: 900_000 });
await meetstream.transcripts.listForBot(botId);
await meetstream.transcripts.transcribeBotAudio(botId);

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

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();

Setup guides: Google Calendar and Outlook.

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 });

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 and MIA configurations.

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 });

Guides: Google signed-in bots, Zoom OBF, custom storage.

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.

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().

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.

Errors

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

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); }
}

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

Configuration

const meetstream = new MeetStream({
apiKey: process.env.MEETSTREAM_API_KEY,
baseUrl: 'https://api.meetstream.ai/api/v1',
timeout: 60_000,
maxRetries: 2,
fetch: customFetch,
});
VariablePurpose
MEETSTREAM_API_KEYYour key. Required unless passed explicitly
MEETSTREAM_API_URLOverride 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:

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

Source and support

Prefer a different surface? The MCP server exposes the same capability as tools for coding agents, and the CLI covers it from your terminal. Runnable examples live in Labs.