Zoom Meeting Bots
Send API-controlled bots into Zoom meetings to record, transcribe, and interact
Connect your Zoom app once, then send a bot into any supported Zoom meeting with a single create_bot call.
To connect multiple Zoom apps and users and use mapped PMI or scheduled meetings, see Zoom OAuth Meeting Bots.
Zoom at a glance
How a Zoom bot joins
MeetStream bots join Zoom through the Zoom Meeting SDK. The SDK always runs as a Zoom Marketplace app, so every Zoom bot needs a Meeting SDK app’s credentials. MeetStream uses the Client ID and Client Secret you save in the dashboard to authorize the SDK, then joins the meeting with the display name from bot_name.
Create a Meeting SDK app in the Zoom App Marketplace
Create a General app, turn on Meeting SDK under Features > Embed, and copy the app’s Client ID and Client Secret. The full walkthrough is in Zoom Marketplace App Setup.
Save the credentials in MeetStream
Open Integrations in the sidebar, select the Zoom tab, click Configure credentials (or Update credentials), paste the Client ID and Client Secret, and save. The card shows Credentials active when they are stored.

Create a bot
Call POST /api/v1/bots/create_bot with a Zoom /j/ link. MeetStream detects Zoom from the link, so there is no platform field.
If no Zoom credentials are saved, create_bot returns HTTP 400 with Zoom bots require Zoom credentials in user profile (or, in a later check, Zoom bots require a connected Zoom app or Zoom credentials in the user profile). Save the credentials and retry.
Which meetings your app can join
Zoom decides which meetings a Meeting SDK app may join. Since March 2, 2026, Zoom requires Meeting SDK apps that join meetings outside the app’s own Zoom account to be reviewed by Zoom and to join with a ZAK or an OBF token (Zoom: Meeting SDK authorization, Zoom: OBF FAQ).
When Zoom refuses a join to a meeting outside your app’s account, the bot usually ends with Stopped and the message Unable to join external meeting. To get past it, submit your app for review (Zoom App Production Submission) and use a ZAK or OBF join.
Guest, signed-in and on-behalf-of joins
Send only one of the two URLs. MeetStream calls your URL at join time and uses the token it returns. If the call fails, the bot does not fall back to a guest join; it ends with Error: Failed to join meeting: ZAK token is required but could not be fetched (or the OBF equivalent). MeetStream’s hosted token endpoints are retired, and the old use_zoom_obf and zoom_oauth_connection_user_id fields are rejected with HTTP 400. The full contract (request headers, GET and POST behavior, response formats, test commands) is in Zoom Authenticated Bots (ZAK and OBF).
Supported meeting links
The API accepts http or https links on zoom.us, any subdomain of zoom.us (such as us05web.zoom.us or yourcompany.zoom.us) and zoomgov.com. The bot then reads the meeting ID from a /j/<meeting-id> path and the passcode from the pwd query parameter.
If you only have a personal room or vanity link, open it in Zoom and copy the meeting’s invite link instead. It contains /j/<meeting-id> and, when the meeting has a passcode, ?pwd=.
There is no separate passcode field. Keep the pwd value in the link exactly as Zoom generated it. If the passcode is missing or wrong, Zoom usually leaves the bot connecting, and after 60 seconds the bot ends with Error: Failed to connect to meeting. Some passcode errors surface as Stopped with Meeting failed instead.
Joining, the waiting room and admission
What to expect at each step:
bot.joiningis sent by the API when the bot is accepted. For scheduled bots, statuses start at the scheduled time.bot.in_waiting_room(InWaitingRoom, messageBot is waiting to be admitted) fires as soon as Zoom accepts the join request, even if the meeting has no waiting room. On Zoom it means “waiting to be let in”. If there is no waiting room,bot.inmeetingfollows within seconds.- The admission timer starts at the same moment and runs for
automatic_leave.waiting_room_timeoutseconds (60 to 1200, default 600). For a scheduled bot, the timer is extended by the time left until the scheduled start, so it effectively counts fromjoin_at. - One timer covers two situations: the bot sitting in the waiting room, and the bot waiting for a host who has not started the meeting yet. When the timer runs out in either case, the bot ends with
NotAllowedandFailed: Not admitted to meeting(bot.notallowed,status_code: 500). The message is the same even when the real cause was that the host never started the meeting. bot.inmeeting(InMeeting,Bot successfully joined the meeting) fires once Zoom lets the bot in. The recording permission step follows.
Depending on how Zoom reports a meeting that has not started, the attempt can also end at once (without waiting) with Stopped and Meeting not yet started, or Meeting does not exist or host has not started it yet.
If Zoom shows a meeting reminder or disclaimer (for example a recording notice), the bot accepts it automatically.
Zoom settings that affect admission
The host manages these settings in the Zoom web portal.
If your Zoom account hosts the meetings, support can connect your Zoom app so that bots join with fewer prompts. Contact support to set this up.
Recording permission
Zoom only lets a Meeting SDK participant capture meeting audio and video with the host’s permission to record locally (Zoom’s record to computer permission). Zoom’s Meeting SDK review requirements expect participant apps to request that permission from the host. About one second after it is admitted, the bot checks whether it can record:
- It can already record: recording starts and
bot.recordingfires. No permission events are sent. - It cannot record yet: the bot asks the host for permission once, through Zoom’s standard request, and starts a timer of
automatic_leave.recording_permission_denied_timeoutseconds (60 to 300, default 60, Zoom only).
Things to know:
- The bot asks once. It does not re-request after a denial or a timeout; it leaves.
- If the host revokes recording permission later in the meeting, the bot stops recording and leaves the same way, with
Failed: Recording permission denied by host. - The terminal event after a permission denial is a clean
bot.stopped(bot_event: "bot.stopped"), notbot.denied.bot.deniedis reserved for Zoom refusing the join itself. - If recording still cannot start after permission is granted (one retry after 2 seconds), the bot ends with
Error: Failed to start recording. - While a Zoom bot records, its status (from get bot status and the dashboard) stays
RecordingPermissionAllowedorInMeeting.Recordingappears in the status timeline and as thebot.recordingwebhook.
To receive the permission events on a workspace endpoint, tick them under Zoom recording permission when you create or edit the endpoint in Webhooks.

Zoom settings that affect recording permission
Outcomes: every way a Zoom bot ends
Every terminal status is preceded by Leaving (bot.leaving, Bot is leaving the meeting). On the wire, terminal webhooks carry event: "bot.stopped" plus a specific bot_event; status_code is 500 for NotAllowed, Denied and Error, and 200 for Stopped and Kicked. The full cross-platform list is in Bot status and error codes.
Some Zoom join results use Stopped with status_code: 200 (bot_event: "bot.stopped"): Meeting not yet started, Meeting does not exist or host has not started it yet, Unable to join external meeting and Meeting failed. Read the message field to find the cause.
Before the bot is admitted
Other messages that start with Error: (for example SDK start-up errors) mean the bot could not start its Zoom session. Retry once, then contact support with the bot ID.
After the bot is in the meeting
Errors when you create the bot (HTTP 400)
These come back from create_bot before any bot is started. The general error format is on the Errors page.
Automatic leave on Zoom
Set these under automatic_leave on create_bot. Zoom applies some of them differently from Google Meet and Teams. This table shows the behavior on Zoom. The field reference is in Automatic Leave Configuration.
For a fixed maximum length on Zoom, set video_required: true or audio_separate_streams: true so that in_call_recording_timeout applies, or call remove bot from your backend when your own logic decides the meeting is over.
Media, chat and interaction
Transcription on Zoom
Post-call transcription works on Zoom with every post-call provider (deepgram, assemblyai, jigsawstack, sarvam, meetstream). See Post-call transcription.
For live transcription, set live_transcription_required.webhook_url and one streaming provider under recording_config.transcript.provider:
Use one of these four providers for live transcripts on Zoom. The bot feeds the provider each participant’s speech separately, so live transcripts carry the Zoom participant’s name and ID for every utterance. Only one streaming provider runs per bot. Provider details are in Transcription providers.
Meeting scope on Zoom
- The bot joins regular Zoom meetings from
/j/invite links and stays in the main meeting room. - For transcripts, use a live or post-call provider (Transcription on Zoom).
- Contact support before you send bots to ZoomGov meetings.
Watch your Zoom bots
In the dashboard, All Bots lists every bot with its platform and status. Filter by Zoom and a status to find failed joins, and open a bot to see its status timeline and messages.

To follow a bot live from your terminal, use meetstream listen. The Debugging guide walks through reading statuses and webhooks.
Zoom FAQ
Why does create_bot return "Zoom bots require Zoom credentials in user profile"?
No Zoom Meeting SDK credentials are saved for your account. Create a General app with Meeting SDK enabled (Zoom Marketplace App Setup), then save its Client ID and Client Secret in Integrations > Zoom.
Why does my bot fail with "Invalid meeting_link format"?
The bot joins from /j/<meeting-id> links. Personal room (/my/), webinar (/w/), /s/ and /wc/join/ links pass the API check and end with this message when the bot joins. Copy the meeting’s invite link, which uses /j/.
Why does my bot end with "Unable to join external meeting"?
Zoom does not let your app join meetings hosted outside its own Zoom account. Until Zoom reviews your app, it can only join your own account’s meetings. After review, external meetings need a ZAK or OBF join. See Which meetings your app can join.
Why is my bot stuck in the waiting room, then ends with NotAllowed?
Only a host or co-host can admit the bot, and they must do it before waiting_room_timeout (default 600 s, max 1200). The same timer also runs out when the host never starts the meeting, with the same Failed: Not admitted to meeting message. Check that the host is present, raise the timeout, or change who goes to the waiting room in the host’s Zoom settings.
Why did bot.in_waiting_room fire when my meeting has no waiting room?
On Zoom, bot.in_waiting_room fires as soon as Zoom accepts the bot’s join request. It means “waiting to be let in”. Without a waiting room, bot.inmeeting follows within seconds.
Why does my Zoom bot join but never record?
Recording needs the host’s permission on Zoom. If the host denies it, or does not answer within recording_permission_denied_timeout (default 60 s, max 300), the bot leaves with bot.recording_permission_denied followed by bot.stopped. Make sure the host’s account has Record to computer files on, and consider Auto approve their permission requests for external participants.
Why does my bot's status say RecordingPermissionAllowed instead of Recording?
On Zoom, Recording is written to the status timeline and sent as bot.recording, but the bot’s current status stays RecordingPermissionAllowed (or InMeeting when no permission request was needed). Treat either as “in the meeting and recording” once you have received bot.recording.
Why did my bot end with "Error: Failed to connect to meeting"?
The bot was still connecting after 60 seconds. This is usually a missing or wrong passcode. Pass the full invite link, including ?pwd=, as meeting_link.
Why did my bot fail with "OBF token is required but could not be fetched"?
MeetStream called your obf_url and did not get a usable token: a non-2xx response, a timeout (15 s), a short or empty body, or JSON without a token, zak or obf field. Also check that you did not add meeting_number to the URL yourself. See Zoom Authenticated Bots.
How do I end a bot in a Zoom meeting that nobody else joins?
On Zoom, end a session that nobody else joins with in_call_recording_timeout (when the bot records video or separate streams) or with a remove bot call from your backend. everyone_left_timeout starts after a participant leaves.
Which transcription providers work on Zoom?
For live transcripts, use deepgram_streaming, assemblyai_streaming, sarvam_streaming or meetstream_streaming. For post-call transcripts, use any post-call provider. meeting_captions is for Google Meet and Microsoft Teams.
Can a Zoom bot join a meeting that only allows signed-in users?
Not as a guest. Use a signed-in join: host a zak_url that returns a ZAK for a Zoom user who is allowed into the meeting. See Zoom Authenticated Bots.
