Gareth Wilson Gareth Wilson

Guide to Meeting BaaS Webhooks: Features and Best Practices

Published


Meeting BaaS sends recording bots into Zoom, Google Meet and Microsoft Teams calls through an API, and can schedule them from your users' connected calendars. A bot's life is mostly asynchronous: it queues, joins, waits to be admitted, records, leaves, and then processes the transcript and media. Meeting BaaS webhooks report each of those steps, the chat messages posted during the call, and changes in connected calendars, so your app can react without polling bot status.

What are Meeting BaaS webhooks?

In API v2, webhooks are configured per account. Each endpoint you add in account settings receives every bot and calendar event for every bot, and Meeting BaaS delivers them through Svix.

AspectValue
MethodPOST, JSON body
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
SuccessOnly 2xx. Redirects (3xx), 4xx and 5xx are all retried
Timeout30 seconds
DeliveryAt-least-once, generally in order but not guaranteed. Deduplicate on svix-id

Meeting BaaS also has callbacks, set per bot with callback_config when you create it. They are separate from webhooks: sent directly rather than through Svix, authenticated with a static x-mb-secret header instead of a signature, and retried only four times over about 21 seconds. This guide covers account-level webhooks.

Meeting BaaS webhook features

FeatureDetails
ConfigurationAccount settings only, not the API
ScopeEvery endpoint gets all bot and calendar events
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature
SecretPer endpoint. Format not shown in the docs
BodyJSON
Event routingBody field event
Retries7 attempts in about 3 hours
ResendPOST /v2/bots/:bot_id/resend-webhook
Source IPsNot published

All 12 events are handled in the skill's example receivers.

Events and payloads

Bot events and calendar events share a top-level shape:

FieldMeaning
eventThe event name. Branch on this
dataThe event payload
data.bot_idThe bot (bot events)
data.event_idBot events carry an event_id in data
data.extraEchoes the extra object you set when creating the bot
extra (top level)Where bot.chat_message puts it: beside data, not inside

Calendar payloads are keyed by data.calendar_id. Calendar event payloads also have a data.event_id inside each instance, which identifies the calendar event, not the webhook.

EventTriggered WhenCommon Use Cases
bot.status_changeA bot's status changes. data.status.code is the new status, with created_at, start_time (only on in_call_recording) and error_message on failuresTrack bot progress, update UI
bot.completedA bot finished recording and processing. Carries transcription, mp4, audio, diarization, chat_messages URLs, duration_seconds, participants, speakersDownload recordings, post-meeting workflows
bot.chat_messageA participant posts in the meeting chat. message_id, sender_name, sender_id (may be null), text, sent_at (when the webhook was dispatched)Chat integrations, compliance logging
bot.failedA bot failed. error_code (e.g. BOT_NOT_ACCEPTED, TIMEOUT_WAITING_TO_START, INSUFFICIENT_TOKENS, DAILY_BOT_CAP_REACHED, INVALID_MEETING_PLATFORM, TRANSCRIPTION_ERROR) and error_messageError handling, retry bot creation
calendar.connection_createdA calendar connection was created (platform, account_email, calendar_name)Confirm the integration
calendar.connection_updatedA connection was updated, e.g. credentials refreshed (status)Monitor connection health
calendar.connection_deletedA connection was deletedClean up calendar data
calendar.connection_errorA connection hit an error, e.g. OAuth refresh failed (error, status)Alert the user, reconnect
calendar.events_syncedA calendar sync finished, when it syncs for the first time (events_synced, sync_type)Process newly synced events
calendar.event_createdA new event in a connected calendar (event_type, series_id, series_bot_scheduled, instances)Schedule bots for new events
calendar.event_updatedAn event changed (affected_instances)Update bot schedules
calendar.event_cancelledAn event was cancelled (cancelled_instances)Cancel scheduled bots

The three calendar.event_* examples and bot.completed are abbreviated in Meeting BaaS's docs, so check real payloads before you rely on their nested fields.

data.status.code on bot.status_change can be any of queued, pickup_delayed, transcribing, completed, failed, joining_call, in_waiting_room, in_waiting_for_host (Zoom only), in_call_not_recording, in_call_recording, recording_paused, recording_resumed, call_ended, recording_succeeded, recording_failed, and the Google Meet and Teams signals api_request_stop, bot_rejected, bot_removed, bot_removed_too_early, waiting_room_timeout, invalid_meeting_url and meeting_error. If you only need outcomes, use bot.completed and bot.failed.

This is the bot.chat_message sample from the webhook samples library. extra, your own metadata from bot creation, sits beside data here rather than inside it:

{
  "event": "bot.chat_message",
  "data": {
    "bot_id": "123e4567-e89b-12d3-a456-426614174000",
    "event_id": "789e0123-e45b-67c8-d901-234567890abc",
    "message_id": "spaces/XxM_aNTpzGkB/messages/1773539019302815",
    "sender_name": "John Doe",
    "sender_id": 2,
    "text": "Can we discuss the Q4 roadmap?",
    "sent_at": "2025-01-15T10:32:15Z"
  },
  "extra": {
    "customer_id": "12345"
  }
}

bot.chat_message covers participants' messages only. The bot's own messages are in the chat_messages file linked from bot.completed.

Setting up Meeting BaaS webhooks

  1. Open your Meeting BaaS account settings and add a webhook endpoint with your HTTPS URL, for example https://example.com/webhooks/meeting-baas.
  2. Copy the endpoint's signing secret into MEETING_BAAS_WEBHOOK_SECRET.
  3. There's no per-event subscription, so your handler must return 2xx for events it ignores.

Meeting BaaS's docs never show a secret value. They say v2 webhooks go through Svix and should be verified with Svix's libraries, and Svix endpoint secrets look like whsec_<base64>. Store the value exactly as copied. If it doesn't start with whsec_, see the skill's verification reference for the key derivation to adapt.

To receive Meeting BaaS through Hookdeck, create a source with the Meeting BaaS type, paste the secret, and add the source URL as the endpoint. Then forward it locally with the Hookdeck CLI:

npx hookdeck-cli listen 3000 meeting-baas --path /webhooks/meeting-baas

Send a bot into a test meeting and watch the bot.status_change events arrive as it joins and records.

Only Meeting BaaS v2 account-level webhooks are supported. Per-bot callbacks and v1 API webhooks aren't signed this way and fail verification.

Securing Meeting BaaS webhooks

The check is Svix's: HMAC-SHA256 over <svix-id>.<svix-timestamp>.<raw body> with the base64-decoded part of the secret after whsec_, compared against each v1, entry in svix-signature. This function falls back to webhook-* header names, as the Hookdeck source does:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Meeting BaaS sets no timestamp tolerance; the Svix SDK applies five minutes. Hookdeck applies none and replays or retries with the original svix-timestamp, so behind Hookdeck the SDK's window would reject real events. The verifier above checks only the signature.

bot.completed links expire after four hours, so the receiver stores the event and a worker downloads the files straight away:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// botEvents.put(messageId, event) saves the event and resolves once it is durable.
function createMeetingBaasReceiver(botEvents) {
  const app = express();
  app.post('/webhooks/meeting-baas', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.MEETING_BAAS_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    try {
      await botEvents.put(req.headers['svix-id'] || req.headers['webhook-id'], event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200); // ignored events must still get 2xx
  });
  return app;
}

When Meeting BaaS posts to a Hookdeck source, Hookdeck answers 200 even if the signature fails, records the request as failed verification and stops it there.

Make Meeting BaaS webhooks production-ready. Hookdeck Event Gateway verifies each bot and calendar event, routes recordings to your media pipeline and keeps a replayable history.

Meeting BaaS webhook limitations and pain points

The Problem: A bot.completed event processed late has dead links to the transcript and video.

Why It Happens: The presigned URLs are valid for four hours. Meeting BaaS keeps its own retries, about three hours in total, inside that window, but a backlog on your side can still outlast it.

Workarounds:

  • Download artefacts as the first step after receiving bot.completed.
  • Watch the age of bot.completed events in your queue and alert well before four hours.

How Hookdeck Can Help: Hookdeck can route bot.completed to a dedicated download worker with filters, so it never waits behind status updates.

Three hours of retries, then nothing

The Problem: Events from a long outage are gone.

Why It Happens: Meeting BaaS makes seven attempts over about three hours. If an endpoint fails every delivery for five days it's disabled, and later messages are dropped silently until you re-enable it.

Workarounds:

  • Set up a "Webhook Delivery Exhausted" alert rule (webhook_delivery_exhausted).
  • Resend individual bots' webhooks with the resend endpoint:
You want toUse
Resend a bot's webhookPOST /v2/bots/:bot_id/resend-webhook
Know when retries are exhaustedConfigure a "Webhook Delivery Exhausted" alert rule (webhook_delivery_exhausted)
Re-enable a disabled endpointDashboard. Endpoints failing every delivery for 5 days are disabled and later messages are silently dropped

How Hookdeck Can Help: Hookdeck accepts each event as it's sent and retries your service for as long as you choose, so Meeting BaaS's own retry window stops mattering.

Every endpoint gets every event

The Problem: A service that only wants calendar events still receives every bot status change.

Why It Happens: Endpoints have no event filter. All bot and calendar events go to all endpoints.

Workarounds:

  • Return 200 early for events a service doesn't handle.

How Hookdeck Can Help: One Hookdeck source can fan events out to several destinations, filtered on event.

Events can arrive out of order

The Problem: bot.completed is processed before the final bot.status_change.

Why It Happens: Delivery is generally in order, but retries can reorder events.

Workarounds:

  • Treat bot.completed and bot.failed as terminal, and ignore later non-terminal statuses for the same bot_id.

How Hookdeck Can Help: Hookdeck's event log shows each bot's events in arrival order, which makes ordering problems easy to trace.

Best practices

Deduplicate on svix-id

Delivery is at least once. Store svix-id and skip repeats; implementing webhook idempotency walks through the options.

Use extra to link bots to your records

Pass your own IDs in extra when you create a bot. They come back in data.extra, or at the top level on bot.chat_message.

Handle bot.failed by error code

INSUFFICIENT_TOKENS and DAILY_BOT_CAP_REACHED point at your account's limits, while BOT_NOT_ACCEPTED and TIMEOUT_WAITING_TO_START come from the meeting itself. Decide per code whether creating a new bot makes sense.

Keep chat out of logs

bot.chat_message carries what participants typed. Log the bot_id and message_id, not the text.

Conclusion

Meeting BaaS v2 webhooks report each bot and connected calendar in detail. Recording links last four hours, so download them as soon as the event arrives.

Hookdeck Event Gateway gives each Meeting BaaS event a longer retry window and routes recordings, chat and calendar changes where they belong. Get a free Hookdeck account.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.