# Meeting BaaS Webhooks

[Meeting BaaS](https://meetingbaas.com) is a meeting-recording bot API for Zoom, Google Meet and
Microsoft Teams. In API v2, **account-level webhooks** report bot status, completion, chat and
failure plus calendar sync events. They are delivered and signed by Svix,
which implements Standard Webhooks. Every delivery is an HTTP `POST` with a JSON body and three
signature headers: `svix-id`, `svix-timestamp`, and `svix-signature`. The body is `{event, data}`:
the event name is the top-level `event`, and `data.extra` echoes the `extra` you set on the bot.

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

Per-bot callbacks (`callback_config` when you create a bot) are a separate channel: direct,
unsigned requests that only carry an optional `x-mb-secret` header. This skill does not cover them.

## When to Use This Skill

* How do I receive Meeting BaaS webhooks?
* How do I verify Meeting BaaS webhook signatures (`svix-signature`)?
* How do I handle `bot.completed`, `bot.status_change` or `bot.failed` events?
* How do I process `bot.chat_message` or `calendar.event_created` events?
* Why is my Meeting BaaS webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.MEETING_BAAS_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["MEETING_BAAS_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meeting-baas-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meeting-baas-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meeting-baas-webhooks/examples/fastapi/).

The signing secret is per endpoint. Meeting BaaS's docs print no secret, but because v2 webhooks
go through Svix it is expected to be a Svix `whsec_<base64>` value, and the examples assume that.
Keep the full `whsec_...` value in `MEETING_BAAS_WEBHOOK_SECRET`. Handlers fail closed: with the
variable unset (or not a `whsec_` value) they return 500 instead of accepting anything. Missing
headers or a bad signature get a 400.

### Hookdeck retries and replays hit the 5-minute window

The `svix` library always enforces a fixed 5-minute timestamp window, and it can't be configured.
Hookdeck retries and replays resend the **original** `svix-timestamp`, so the library rejects any
retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on `svix-id` instead of relying on the timestamp.

## Common Event Types

12 events are documented (the list may not be exhaustive). The name is `event` in the JSON body.
The examples handle all of these and return `200` for anything else:

| Event | Triggered When |
| --- | --- |
| `bot.status_change` | A bot's status changes (`data.status.code`, e.g. `in_call_recording`) |
| `bot.completed` | A bot finished recording and processing (presigned artifact URLs, valid 4 hours) |
| `bot.chat_message` | A participant posts in the meeting chat (`extra` is beside `data`) |
| `bot.failed` | A bot failed (`data.error_code`, `data.error_message`) |
| `calendar.connection_created` | A calendar connection was created |
| `calendar.connection_updated` | A calendar connection was updated |
| `calendar.connection_deleted` | A calendar connection was deleted |
| `calendar.connection_error` | A calendar connection hit an error, e.g. OAuth refresh failed |
| `calendar.events_synced` | A calendar's events were synced (first sync) |
| `calendar.event_created` | An event was created in a connected calendar |
| `calendar.event_updated` | An event was updated in a connected calendar |
| `calendar.event_cancelled` | An event was cancelled in a connected calendar |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/overview.md) for payload shapes and delivery rules.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
MEETING_BAAS_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 meeting-baas --path /webhooks/meeting-baas

```

Add the tunnel URL as a webhook endpoint in your Meeting BaaS account settings (not via the API)
and copy its signing secret. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/overview.md) - Meeting BaaS webhook concepts, events, payloads, delivery rules
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/setup.md) - Add an endpoint, get the secret, resend, auto-disable
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meeting-baas-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window