meetstream-migrate
One-command migration from Recall.ai to MeetStream.ai. Scans your codebase, maps every API call, shows the diff, and migrates — safely, with warnings for anything that needs a human look.
v2.1.2 — near feature parity with Recall.ai (verified live against production, July 2026). MeetStream matches Recall across recording, per-participant streams, real-time audio/video/ transcript, chat + image output, Google and Outlook calendar, scheduling, signed-in bots, and retention — plus native AI summaries and MIA voice agents that Recall doesn't have. The only gaps: pause/resume recording and live video output into the call (both shipping soon). See Feature parity & gaps below.
Quick Start
pip install meetstream-migrate
meetstream-migrate scan ./your-recall-project
Commands
# Scan (read-only) — finds every Recall.ai reference, writes NOTHING
meetstream-migrate scan ./your-project
# Dry run — shows exactly what would change, still writes nothing
meetstream-migrate migrate ./your-project --dry-run
# Migrate — applies all transformations (prompts before writing)
meetstream-migrate migrate ./your-project --yes
# Verify your MeetStream credentials + API reachability
meetstream-migrate test --api-key YOUR_MEETSTREAM_API_KEY
CLI reference
meetstream-migrate <command> [path] [options]
Commands:
scan [path] Scan only — show what would change
migrate [path] Scan & migrate a project
test Run API tests against MeetStream.ai
Options:
--yes, -y Skip the confirmation prompt
--dry-run Show changes without writing files
--api-key KEY MeetStream API key (for the test command)
--verbose, -v Detailed output
--help, -h Show help
What it does
1. Scan — recursively finds:
- Recall.ai base URLs (
us-east-1.recall.ai,eu-west-1.recall.ai,api.recall.ai) - API endpoint calls (
/api/v1/bot/,/api/v1/bot/{id}/audio/, etc.) - Recall-specific field names (
meeting_url,recording_mode,transcription_options,metadata, …) - Webhook handlers (
bot.status_change,joining_call,in_call_recording,call_ended,done,fatal, …) - SDK imports (
require('recall-ai'),import recall) - Environment variables (
RECALL_API_KEY,RECALL_API_URL,RECALL_WEBHOOK_SECRET)
Supported file types: .js .ts .jsx .tsx .mjs .cjs .py .go .rb .java .kt .rs .php .cs .swift .env .yaml .yml .json .toml .ini .cfg
2. Display — endpoint mapping table, field mapping table, webhook event mapping, and a compatibility check (auto-migratable vs. needs manual review).
3. Transform — base URL swap, endpoint path rewrites, field renames, webhook event renames, env var renames, import/require updates. Every change carries an inline warning when it needs a second look.
4. Report — what changed, what needs manual review, and a post-migration checklist.
Endpoint Mapping
| Recall.ai | MeetStream.ai | Notes |
|---|---|---|
POST /api/v1/bot/ |
POST /api/v1/bots/create_bot |
Field names differ (see below) |
GET /api/v1/bot/{id}/ |
GET /api/v1/bots/{id}/detail |
Also GET /api/v1/bots/{id}/status |
POST /api/v1/bot/{id}/leave_call/ |
GET /api/v1/bots/{id}/remove_bot |
⚠️ Method changes POST → GET |
DELETE /api/v1/bot/{id}/ |
DELETE /api/v1/bots/{id}/delete |
|
GET /api/v1/bot/ |
GET /api/v1/bots |
List all bots |
GET /bot/{id}/audio/ |
GET /bots/{id}/get_audio |
Presigned S3 URL, valid 1h |
GET /bot/{id}/video/ |
GET /bots/{id}/get_video |
Presigned S3 URL, valid 10min |
GET /bot/{id}/transcript/ |
GET /transcript/{transcript_id}/get_transcript |
⚠️ Uses transcript_id, not bot_id |
GET /bot/{id}/participants/ |
GET /bots/{id}/get_participants |
|
GET /bot/{id}/chat_messages/ |
GET /bots/{id}/get_chats |
|
POST /bot/{id}/send_chat_message/ |
POST /bots/{id}/send_message |
REST body { "message": "..." } — drop Recall's to/pin fields |
POST /bot/{id}/output_video/ |
POST /bots/{id}/send_image |
Public img_url (not base64) |
POST /bot/{id}/output_audio/ |
WebSocket sendaudio command |
⚠️ Manual — no REST equivalent, use socket_connection_url bridge |
POST /bot/{id}/output_media/ |
WebSocket bridge (partial) | ⚠️ Manual — covers audio + static image today |
POST /bot/{id}/pause_recording/ |
— | ⛔ Not yet supported (shipping soon) |
POST /bot/{id}/resume_recording/ |
— | ⛔ Not yet supported (shipping soon) |
GET /bot/{id}/screenshots/ |
GET /bots/{id}/get_screenshots |
|
GET /bot/{id}/speaker_timeline/ |
GET /bots/{id}/get_speaker_timeline |
|
POST /api/v1/webhook/ |
callback_url field on create_bot |
Per-bot, not account-level |
GET /bot/{id}/calendar/… |
POST /calendar/create_calendar (underscore) |
Body: google_client_id/google_client_secret/google_refresh_token |
Field Mapping
| Recall.ai | MeetStream.ai | Notes |
|---|---|---|
meeting_url |
meeting_link |
|
recording_mode (string) |
video_required (boolean) |
"speaker_view"/"gallery_view" → true, "audio_only" → false |
metadata |
custom_attributes |
Values must be strings |
transcription_options |
recording_config.transcript.provider.{provider} |
Object structure differs — see provider config |
noone_joined_timeout |
voice_inactivity_timeout |
|
destination_url |
webhook_url (nested in live_transcription_required) |
|
assembly_ai |
assemblyai |
Also: assemblyai uses speech_models[] + language_code, not model/language |
output_media.websocket.url |
socket_connection_url.websocket_url |
Webhook Events — live-verified against production
MeetStream's envelope key is event — the same key Recall uses. No envelope rename needed. Alongside it: bot_id, message, status_code (200 success / 500 failure), custom_attributes, and on lifecycle events, bot_status.
{ "event": "bot.inmeeting", "bot_id": "...", "bot_status": "InMeeting",
"message": "...", "status_code": 200, "custom_attributes": {} }
Two-layer model: bot.stopped fires once; bot_status tells you why — Stopped (normal) / NotAllowed (lobby timeout) / Denied (host denied) / Error (crash). Recall's separate end-reason events collapse into this single event + status.
| Recall.ai Event | MeetStream event |
bot_status |
status_code |
|---|---|---|---|
bot.joining_call |
bot.joining (may fire up to 3×) |
Joining |
200 |
bot.in_waiting_room |
bot.in_waiting_room |
InWaitingRoom |
200 |
bot.in_call_not_recording |
bot.inmeeting |
InMeeting |
200 |
bot.in_call_recording |
bot.recording |
Recording |
200 |
| (none — new) | bot.leaving |
Leaving |
200 |
| (none — new) | bot.error (non-terminal streaming-provider error; bot keeps running) |
InMeeting |
— (omitted) |
bot.call_ended |
bot.stopped |
Stopped |
200 |
bot.call_ended (lobby timeout) |
bot.stopped |
NotAllowed |
500 |
bot.call_ended (host denied) |
bot.stopped |
Denied |
500 |
bot.fatal |
bot.stopped |
Error |
500 |
recording.done |
manifest.completed → audio.processed → video.processed (if video_required) |
— | 200 |
transcript.done |
transcription.processed |
— | 200 |
transcript.failed |
transcription.failed |
— | 500 |
| (post-call) | bot.done — post-call providers only |
Done |
200 / 500 |
| (retention) | data_deletion |
— | 200 |
Streaming-only providers (deepgram_streaming, assemblyai_streaming, meeting_captions) do not fire transcription.processed/transcription.failed/bot.done — their terminal event is audio.processed. Don't wait on bot.done for a streaming bot, or your handler hangs forever.
transcript_id is never in the webhook payload. Resolve it via GET /bots/{id}/detail (bot_details.transcript_id), the create_bot response, or GET /bots/{id}/transcriptions.
Full bot_status enum: Joining, InWaitingRoom, InMeeting, Recording, Leaving, Stopped, NotAllowed, Denied, Error, Done.
Real-time events
| Recall.ai | MeetStream | Notes |
|---|---|---|
transcript.data / transcript.partial_data |
delivered to live_transcription_required.webhook_url |
Chunk shape: { speakerName, timestamp, transcript, words[] } |
participant_events.join / .leave |
same event names, requires recording_config.realtime_endpoints |
Different payload shape — nested under data.bot.{id,metadata}, no top-level bot_id |
Breaking changes to review manually
- Transcript retrieval uses
transcript_id(from thecreate_botresponse,/detail, or/transcriptions), notbot_id. recording_mode(Recall string) →video_required(MeetStream boolean).- Chat messages: Recall's separate
POST /send_chat_message/→ MeetStream'sPOST /bots/{id}/send_message. - Webhook registration: Recall registers globally via
POST /webhook/; MeetStream usescallback_urlper bot at creation. - Webhook event values rename (see table above) — but the envelope key stays
event, so most handler code needs a value rename, not a restructure. - Post-call processing arrives as discrete events (
manifest.completed,audio.processed,transcription.processed/transcription.failed,video.processed,bot.done) instead of onebot.done-then-fetch signal. - Live transcription:
real_time_transcription.destination_url→live_transcription_required.webhook_urloncreate_bot. - Pause/resume recording has no MeetStream equivalent yet — remove these calls, or segment via
remove_bot+ re-create. - Output audio/media into the call is REST on Recall, WebSocket-bridge on MeetStream — needs a real refactor, not a rename (flagged, not auto-migrated).
Post-Migration Checklist
- Set
MEETSTREAM_API_KEY(get one at app.meetstream.ai/api-keys) - Review every ⚠️ warning from the migration output
- Update webhook handlers: keep branching on
event, rename the values, and checkbot_statusforbot.stopped's reason - Store
transcript_idfor transcript retrieval — fromcreate_bot,/detail, or/transcriptions - Add handlers for
manifest.completed,audio.processed,transcription.processed/failed,video.processed - Verify the
recording_mode→video_requiredboolean logic - Remove any
pause_recording/resume_recordingcalls (not supported yet) - Run
meetstream-migrate test --api-key YOUR_KEYto confirm connectivity
Feature parity & gaps
| Recall capability | MeetStream | Migrator behaviour |
|---|---|---|
| Create/list/get/remove/delete bot | ✅ | auto-mapped |
| Recording (audio/video) + retention | ✅ | auto-mapped |
| Per-participant streams | ✅ | get_audio_streams / get_recording_streams |
| Real-time audio/video/transcript | ✅ | live_audio_required / live_video_required / live_transcription_required |
| Send chat message | ✅ | send_chat_message → REST send_message |
| Output image/camera frame | ✅ | output_video → send_image (public URL) |
| Calendar (Google + Outlook) + scheduling | ✅ | calendar/events/{id}/bot → calendar/schedule/{id} |
| Signed-in / OAuth bots | ✅ | Google login domains/logins; Zoom OBF |
| AI summaries | ✅ (MeetStream-only) | GET /bots/{id}/summary |
| Conversational voice agents (MIA) | ✅ (MeetStream-only) | agent_config_id on create_bot |
| Output audio / interactive media | ⚠️ partial | WebSocket bridge (manual, flagged) |
| Pause/resume recording | ⛔ not yet | flagged — shipping soon |
| Live video output into the call | ⛔ not yet | send_image covers static frames only |
Run scan first to see exactly which of these appear in your codebase before migrating.
Requirements
Python ≥ 3.8
Development
pip install -e .
python -m pytest test/ # or: python -m meetstream_migrate.cli test --api-key YOUR_KEY
License
MIT © MeetStream.ai — meetstream.ai · docs.meetstream.ai
Metadata
Release files for meetstream-migrate 2.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| meetstream_migrate-2.3.0.tar.gz | 22.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| meetstream_migrate-2.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.7 kB
Release files / meetstream_migrate-2.3.0.tar.gz
| Download URL | meetstream_migrate-2.3.0.tar.gz |
|---|---|
| Size | 22.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6c370609389fa35ed9818e59483cafd5c2d3d364bfb75f149bdb1b161334a97
|
|
BLAKE2b-256 checksum How to use checksums |
dc259fae6c6ac3d861f8fc8fdd2cfccfc83705a029f86ea95d909eeab0daabae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / meetstream_migrate-2.3.0-py3-none-any.whl
| Download URL | meetstream_migrate-2.3.0-py3-none-any.whl |
|---|---|
| Size | 19.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
47e29ef0c111fc906bf4fd8e962c26d2864d8e6ad38f99132b3d8a595cea91b6
|
|
BLAKE2b-256 checksum How to use checksums |
6b36732e6c2faf411d5ddba3cf4e9119cf38b049228e23a96d8753192c41ac5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|