MeetStream Guide: Webhook Events

View as Markdown

This guide explains how to receive webhook events from MeetStream, what each event means, and how to trigger follow-up actions like fetching audio, video, or transcription.

Applies to: Google Meet, Zoom, Microsoft Teams.
Support: docs.meetstream.ai • API: api.meetstream.ai


1) Add your webhook URL while creating the bot

When you create a bot, include your webhook endpoint in the payload as:

1{
2 "callback_url": "{{webhook_url}}"
3}

MeetStream will send HTTP POST requests to your callback_url whenever the bot’s status changes during its lifecycle and when post-call processing completes.

Example (Create Bot + callback_url)

Docs: https://docs.meetstream.ai/api-reference/api-endpoints/bot-endpoints/create-bot

$curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
> -H "Authorization: Token <YOUR_API_KEY>" \
> -H "Content-Type: application/json" \
> -d '{
> "meeting_link": "<YOUR_MEETING_LINK>",
> "video_required": false,
> "callback_url": "https://your-domain.com/webhooks/meetstream"
> }'

Tip: Your webhook should respond quickly with 2xx (e.g., 200) to acknowledge receipt.


2) Webhook payload format

Every webhook notification follows this structure:

1{
2 "bot_event": "bot.joining",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Joining",
5 "message": "Bot is joining the meeting",
6 "status_code": 200,
7 "timestamp": "2026-02-27T07:11:51.863543+00:00",
8 "custom_attributes": {}
9}

Fields

  • bot_event — the event name. Identifies what just happened (bot.joining, bot.in_waiting_room, bot.inmeeting, bot.recording, bot.leaving, bot.stopped, bot.kicked, bot.denied, bot.notallowed, bot.failed, bot.done, audio.processed, transcription.processed, video.processed, data_deletion, etc.). Branch on this field.
  • bot_id — unique identifier for the bot session
  • bot_status — status detail (see Status Reference)
  • message — human readable explanation
  • status_code200 for success stages; 500 for any failure (Error, Denied, NotAllowed)
  • timestamp — ISO-8601 timestamp
  • custom_attributes — echoed back if you provided it when creating the bot

3) Event types (what you’ll receive)

MeetStream sends two categories of webhook events: bot lifecycle events and post-call processing events.

A) Bot lifecycle events

These events track the bot’s full lifecycle, from join to post-call completion:

bot_eventWhat it means
bot.scheduled(Scheduled bots only — when join_at is set) MeetStream accepted the scheduling request and created the schedule. Fires immediately after POST /bots. Confirms the schedule was set; the bot will not actually join until the scheduled_join_time carried on the payload.
bot.joiningMeetStream accepted the bot creation request and dispatched the bot (fired by the API server, not the bot container). For scheduled bots, fires when the scheduled execution time triggers.
bot.in_waiting_roomBot has clicked Join and is now waiting to be admitted. Fires for every bot once between bot.joining and bot.inmeeting — briefly for instant-admit meetings; lingers if the host has a lobby enabled.
bot.inmeetingBot successfully joined and is actively in the meeting
bot.recordingBot started capturing audio/video (fires once after recording actually begins)
bot.recording_permission_allowed(Zoom only) Host granted the bot recording permission. Fires between bot.inmeeting and bot.recording.
bot.recording_permission_denied(Zoom only) Host explicitly denied recording permission OR did not respond within the timeout. Followed by bot.leavingbot.stopped.
bot.leavingBot is exiting the meeting (transitional event before any terminal event)
bot.stoppedClean exit — meeting ended, API stop, host ended, or any other graceful terminal cause. bot_status: "Stopped", status_code: 200.
bot.kickedBot was forcibly removed from the meeting by the host or a participant. bot_status: "Stopped", status_code: 200.
bot.deniedHost explicitly denied the bot’s join request. bot_status: "Denied", status_code: 500. (For Zoom recording-permission denial, see bot.recording_permission_denied.)
bot.notallowedBot was not admitted (waiting-room/lobby timeout). bot_status: "NotAllowed", status_code: 500.
bot.failedUnexpected error during the bot’s lifecycle. bot_status: "Error", status_code: 500.
bot.doneFinal bot status after all processing is complete (successful, failed, or partially completed)

Example payloads

bot.scheduled (scheduled bots only — fires immediately after POST /bots when join_at is set)

1{
2 "bot_event": "bot.scheduled",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Scheduled",
5 "message": "Bot scheduled to join at 2026-05-18T08:10:00+00:00",
6 "status_code": 200,
7 "timestamp": "2026-05-17T19:42:13.000000+00:00",
8 "scheduled_join_time": "2026-05-18T08:10:00+00:00",
9 "custom_attributes": {}
10}

About bot.scheduled: Confirms to webhook-driven integrations that the schedule was accepted. The bot will not actually join the meeting until scheduled_join_time — at which point bot.joining and the normal lifecycle fire. Without subscribing to this event, the only signal that a scheduled bot was created is the 201 response body (or polling GET /bots/{id} and checking Status: "Scheduled").

bot.joining (fired by the API server when the bot is dispatched)

1{
2 "bot_event": "bot.joining",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Joining",
5 "message": "Bot is joining the meeting",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:10:00.000000+00:00",
8 "custom_attributes": {}
9}

bot.in_waiting_room

1{
2 "bot_event": "bot.in_waiting_room",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "InWaitingRoom",
5 "message": "Bot is waiting to be admitted",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:10:05.000000+00:00",
8 "custom_attributes": {}
9}

bot.inmeeting

1{
2 "bot_event": "bot.inmeeting",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "InMeeting",
5 "message": "Bot successfully joined the meeting",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:10:12.000000+00:00",
8 "custom_attributes": {}
9}

bot.recording

1{
2 "bot_event": "bot.recording",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Recording",
5 "message": "Bot started recording",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:10:13.000000+00:00",
8 "custom_attributes": {}
9}

bot.recording_permission_allowed (Zoom only)

1{
2 "bot_event": "bot.recording_permission_allowed",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "RecordingPermissionAllowed",
5 "message": "Recording permission granted by host",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:10:12.500000+00:00",
8 "custom_attributes": {}
9}

bot.recording_permission_denied — denied by host (Zoom only)

1{
2 "bot_event": "bot.recording_permission_denied",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "RecordingPermissionDenied",
5 "message": "Failed: Recording permission denied by host",
6 "status_code": 500,
7 "timestamp": "2026-05-18T08:11:00.000000+00:00",
8 "custom_attributes": {}
9}

bot.recording_permission_denied — host did not respond in time (Zoom only)

1{
2 "bot_event": "bot.recording_permission_denied",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "RecordingPermissionDenied",
5 "message": "Failed: Recording permission timeout",
6 "status_code": 500,
7 "timestamp": "2026-05-18T08:11:12.000000+00:00",
8 "custom_attributes": {}
9}

bot.leaving

1{
2 "bot_event": "bot.leaving",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Leaving",
5 "message": "Bot is leaving the meeting",
6 "status_code": 200,
7 "timestamp": "2026-05-18T09:05:40.000000+00:00",
8 "custom_attributes": {}
9}

Terminal-event payloads

Every terminal scenario fires a single webhook. Branch on bot_event to distinguish the cause.

Clean exit (bot.stopped)

1{
2 "bot_event": "bot.stopped",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Stopped",
5 "message": "Bot exited the call: Meeting ended by host",
6 "status_code": 200,
7 "timestamp": "2026-05-18T09:05:41.000000+00:00",
8 "custom_attributes": {}
9}

Kicked by host/participant (bot.kicked)

1{
2 "bot_event": "bot.kicked",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Stopped",
5 "message": "Bot was removed from the meeting by a participant",
6 "status_code": 200,
7 "timestamp": "2026-05-18T08:45:00.000000+00:00",
8 "custom_attributes": {}
9}

Lobby timeout (bot.notallowed)

1{
2 "bot_event": "bot.notallowed",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "NotAllowed",
5 "message": "Failed: Not admitted to meeting",
6 "status_code": 500,
7 "timestamp": "2026-05-18T08:15:00.000000+00:00",
8 "custom_attributes": {}
9}

Host denied join (bot.denied)

1{
2 "bot_event": "bot.denied",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Denied",
5 "message": "Failed: Host denied join",
6 "status_code": 500,
7 "timestamp": "2026-05-18T08:20:00.000000+00:00",
8 "custom_attributes": {}
9}

Note: For Zoom recording-permission denial, the bot emits bot.recording_permission_denied followed by bot.leavingbot.stoppednot bot.denied. See the bot.recording_permission_denied example above.

Unexpected failure (bot.failed)

1{
2 "bot_event": "bot.failed",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Error",
5 "message": "Error: Failed to connect to meeting (stuck in connecting state)",
6 "status_code": 500,
7 "timestamp": "2026-05-18T08:11:00.000000+00:00",
8 "custom_attributes": {}
9}

bot.done is the terminal event of the post-call pipeline — see Section 3.B below for the full payload, behavior, and its relationship with bot_status: "Done".


Important notes:

  • Every bot starts with bot.joining, fired by the MeetStream API server the moment the bot creation request is accepted and dispatched. For scheduled bots (join_at), the API server also fires bot.scheduled immediately on POST /bots to confirm the schedule was accepted; bot.joining then fires later when the scheduled execution time triggers.
  • bot.in_waiting_room is fired for every bot once the container clicks Join. For meetings without a lobby it fires briefly before bot.inmeeting; for meetings with a host-admit lobby it lingers until the host admits.
  • If the bot joins successfully, you will receive bot.inmeeting.
  • bot.recording is sent once the bot actually starts capturing audio/video. For Zoom this is gated on host-granted recording permission, so there can be a noticeable gap between bot.inmeeting and bot.recording. For Teams/GMeet, bot.recording typically fires within a second of bot.inmeeting.
  • (Zoom only) Between bot.inmeeting and bot.recording, you may receive bot.recording_permission_allowed once the host grants recording permission. If the host denies the request OR does not respond within the configured timeout, you’ll receive bot.recording_permission_denied, followed by the usual bot.leavingbot.stopped terminal sequence (note: the terminal is bot.stopped, not bot.denied — recording-permission denial ends with a clean stop).
  • bot.leaving is emitted briefly before any terminal event (whether normal exit, kick, denial, or failure).
  • The in-meeting lifecycle ends with exactly one terminal webhook: bot.stopped / bot.kicked / bot.denied / bot.notallowed / bot.failed.
  • If the bot fails to join (lobby timeout/denied/error), you may get bot.leaving then a failure terminal event right after bot.joining without a bot.inmeeting.
  • After the in-meeting terminal event, MeetStream runs the post-call pipeline (audio upload, video upload, transcription) and you’ll receive the artifact-specific events (audio.processed, video.processed, transcription.processed) followed by bot.done when the pipeline completes.
  • Failure terminals (bot.denied, bot.notallowed, bot.failed) carry status_code: 500 and message prefixed with "Error: " or "Failed: " (see Section 4). Clean terminals (bot.stopped, bot.kicked) carry status_code: 200.

B) Post-call processing events

After the in-meeting terminal webhook, MeetStream continues processing the recorded session in the background. You will receive additional webhook events as each artifact finishes processing, plus a terminal bot.done once the pipeline is complete and data_deletion if/when the media is removed:

bot_eventWhat it meansbot_status after the event
audio.processedAudio extraction and processing completedMediaProcessing
transcription.processedTranscription generation completedMediaProcessing
video.processedVideo processing completedMediaProcessing
bot.donePost-call pipeline finished — audio/video/transcription artifacts are ready to fetchDone
data_deletionBot media has been deleted (manually via DELETE /bots/{id} or automatically when the retention window elapses)MediaExpired

These events arrive asynchronously after the bot has stopped — the order and timing depend on processing duration for each artifact. The expected order on a healthy bot is roughly:

audio.processed
transcription.processed // (any order vs audio/video)
video.processed
bot.done // pipeline complete
... (later, on retention or manual delete)
data_deletion // media gone

Status field progression during post-call: the live Status column on the bot record (visible via GET /bots/{id}) advances through MediaProcessing (pipeline started) → Done (pipeline finished, matches the bot.done webhook) → MediaExpired (media deleted, matches data_deletion). MediaProcessing is a status-only transition and does not have its own webhook — track the artifact-specific events instead.

Example payloads

audio.processed

1{
2 "bot_id": "5b0ff6e7-3cea-4c9f-a6b4-851c5f11cf4f",
3 "bot_event": "audio.processed",
4 "audio_status": "Success",
5 "message": "Audio processing completed successfully",
6 "status_code": 200
7}

transcription.processed

1{
2 "bot_id": "5b0ff6e7-3cea-4c9f-a6b4-851c5f11cf4f",
3 "bot_event": "transcription.processed",
4 "transcript_status": "Success",
5 "message": "Transcript processing completed successfully",
6 "status_code": 200
7}

video.processed

1{
2 "bot_id": "5b0ff6e7-3cea-4c9f-a6b4-851c5f11cf4f",
3 "bot_event": "video.processed",
4 "video_status": "Success",
5 "message": "Video processing completed successfully",
6 "status_code": 200
7}

bot.done — post-call pipeline finished

1{
2 "bot_event": "bot.done",
3 "bot_id": "6667fd0c-0165-471a-a880-06a1180be377",
4 "bot_status": "Done",
5 "message": "Post-call processing completed",
6 "status_code": 200,
7 "timestamp": "2026-05-18T09:07:30.000000+00:00",
8 "custom_attributes": {}
9}

About bot.done: Always carries status_code: 200 — it signals only that the post-call pipeline has finished, not whether each step succeeded. Inspect the artifact-specific events above (audio.processed, transcription.processed, video.processed, transcription.failed, etc.) for per-step success or failure.

data_deletion — media has been deleted (matches Status=MediaExpired)

1{
2 "bot_id": "5b0ff6e7-3cea-4c9f-a6b4-851c5f11cf4f",
3 "bot_event": "data_deletion",
4 "status": "success",
5 "message": "Bot data deleted successfully",
6 "deleted_objects": 5,
7 "timestamp": "2024-01-15T14:30:00Z",
8 "status_code": 200
9}

About data_deletion: Fires once when the bot’s media is removed from MeetStream’s storage — either when the customer calls DELETE /api/v1/bots/{bot_id} or when the retention window configured at bot-creation time elapses. After this event the bot’s Status column reads MediaExpired and the artifact-fetch endpoints (GET /bots/{id}/audio, /video, /transcript) will return 404/410.


4) Status reference (bot_status)

bot_status gives more detail. The table below shows which bot_status value each event carries, alongside the bot_event name. Note that bot_status for bot.kicked is reported as "Stopped" (not "Kicked") — branch on bot_event to distinguish kicks from clean stops.

bot_status (wire)bot_eventMeaning
Scheduledbot.scheduled(Scheduled bots only) Schedule accepted. Bot will join at scheduled_join_time on the payload.
Joiningbot.joiningBot creation request accepted; bot has been dispatched (fired by the API server, not the container)
InWaitingRoombot.in_waiting_roomBot has clicked Join and is now waiting to be admitted (instant for most meetings; lingers if host has a lobby enabled). Fires for every bot.
InMeetingbot.inmeetingBot is in the meeting (admitted)
Recordingbot.recordingBot started capturing audio/video. For Zoom this fires after the host grants recording permission; for Teams/GMeet it fires on the first audio frame received
RecordingPermissionAllowedbot.recording_permission_allowed(Zoom only) Host granted recording permission. Fires between InMeeting and Recording
RecordingPermissionDeniedbot.recording_permission_denied(Zoom only) Host denied recording permission or did not respond within the timeout. Followed by LeavingStopped
Leavingbot.leavingBot is exiting the meeting (transitional, fires before any terminal event)
Stoppedbot.stoppedClean exit — meeting ended, API stop, host ended, or any other graceful terminal cause. status_code: 200.
Stoppedbot.kickedBot was forcibly removed from the meeting by the host or a participant. Note bot_status is "Stopped", not "Kicked" — branch on bot_event to detect kicks. status_code: 200.
NotAllowedbot.notallowedBot could not join (commonly waiting room/lobby timeout). status_code: 500.
Deniedbot.deniedHost explicitly denied the bot’s join request (for Zoom recording-permission denial, the terminal is the clean bot.stopped). status_code: 500.
Errorbot.failedUnexpected error during lifecycle. status_code: 500.
MediaProcessing(no webhook)Bot has exited the meeting and the post-call pipeline has begun (audio/video upload, transcription). Visible via GET /bots/{id} but does not fire its own webhook — track artifact-specific events (audio.processed, transcription.processed, video.processed) for progress.
Donebot.donePost-call pipeline finished. The live Status column also flips to Done at this moment (matches the bot.done webhook).
MediaExpireddata_deletionBot media has been deleted — either manually via DELETE /bots/{id} or automatically when the configured retention window elapses. The data_deletion webhook carries the matching details.

Failure messages

When a stage fails, status_code is 500 and message carries a prefix you can grep on for alerting:

  • "Error: <details>" — unexpected exceptions (uncaught errors, AWS API failures)
  • "Failed: <details>" — handled/expected failures (validation, timeout, denial)

Examples:

  • "Error: Failed to start recording"
  • "Failed: Recording permission denied by host"
  • "Failed: Not admitted to meeting"
  • "Error: Transcript processing failed"

Your server can react to webhook events and run follow-up workflows.

A) On bot.inmeeting

Typical actions:

  • Update your UI/state: “bot is live”
  • Start timers / internal tracking
  • Notify other systems that recording has started

B) On any terminal event (bot.stopped / bot.kicked / bot.denied / bot.notallowed / bot.failed)

This is the moment to mark the session as complete. Artifacts may still be processing at this point.

Recommended flow:

  1. Receive a terminal webhook
  2. Branch on bot_event:
    • bot.stopped — clean exit (meeting ended, API stop, host ended, etc.). Wait for post-call processing events before fetching outputs.
    • bot.kicked — bot was forcibly removed by host/participant. Recording up to that point is still processed.
    • bot.denied — host explicitly denied join. Log the message and alert/handle accordingly.
    • bot.notallowed — bot was not admitted (lobby/waiting-room timeout). Log and handle accordingly.
    • bot.failed — unexpected error during the bot’s lifecycle. Log message and alert.

C) On post-call processing events

Once you receive audio.processed, transcription.processed, or video.processed, the corresponding artifact is ready to fetch:

D) On data_deletion

Confirms that bot data has been removed. Update your records accordingly — further fetch requests for this bot’s artifacts will fail.


If you configure a webhook secret, MeetStream includes an HMAC signature in headers:

  • X-MeetStream-Signature: sha256=<hex_digest>
  • X-MeetStream-Timestamp: ISO 8601 timestamp

Verification steps:

  1. Compute HMAC-SHA256(your_secret, raw_request_body)
  2. Compare with X-MeetStream-Signature (strip sha256= prefix)
  3. Optionally validate timestamp is within an acceptable window (replay protection)

7) Retry behavior (important)

  • Webhook delivery is best-effort.
  • If your endpoint returns a non-2xx, the webhook is not retried.
  • A bot may send up to 3 bot.joining events if join retries are configured.
  • bot.inmeeting is sent at most once. Exactly one terminal webhook (bot.stopped / bot.kicked / bot.denied / bot.notallowed / bot.failed) is sent per bot.
  • Post-call processing events (audio.processed, transcription.processed, video.processed, bot.done, data_deletion) are sent at most once.

8) Minimal webhook handler checklist

  • ✅ Accept POST requests at callback_url
  • ✅ Verify signature (if enabled)
  • ✅ Always respond 2xx quickly
  • ✅ Idempotent handling (store processed event IDs or de-dupe by {bot_id, bot_event, timestamp})
  • ✅ On any terminal event (bot.stopped / bot.kicked / bot.denied / bot.notallowed / bot.failed), mark session complete
  • ✅ On audio.processed / video.processed / transcription.processed, fetch the corresponding artifact
  • ✅ On data_deletion, clean up local references

If you want, share your preferred stack (Node/FastAPI/Cloudflare Workers), and I’ll provide a ready-to-paste webhook handler example.