# Recall.ai Webhooks

## When to Use This Skill

* How do I receive Recall.ai webhooks for meeting bot status changes?
* How do I verify Recall.ai webhook signatures (`webhook-signature` or `svix-signature`)?
* Why are the `webhook-id` / `webhook-timestamp` / `webhook-signature` headers missing?
* Handling `bot.done`, `recording.done` or `transcript.done` to download media and transcripts
* Handling real-time endpoint events like `participant_events.join` and `transcript.data`
* Keeping bot scheduling in sync with Calendar V2 (`calendar.update`, `calendar.sync_events`)

## Verification (core)

Recall.ai uses Standard Webhooks. With a workspace verification secret (Developers → API Keys & Secrets) every request carries `webhook-id`, `webhook-timestamp` and `webhook-signature`. Accounts created before 2025-12-15 get `svix-id`, `svix-timestamp` and `svix-signature` on Svix webhooks, signed with the per-endpoint Svix secret. The algorithm is the same, so accept either prefix:

```javascript
const prefix = headers['webhook-signature'] !== undefined ? 'webhook' : 'svix';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`]; // "v1,<b64> v1,<b64>"

// Key = base64-decoded part after "whsec_"; signed content = id.timestamp.rawBody
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto.createHmac('sha256', key)
  .update(`${id}.${timestamp}.`).update(rawBody).digest();

// Several signatures arrive after a secret rotation: accept any v1 match
const valid = signature.split(' ').some((entry) => {
  const [version, sig] = entry.split(',');
  if (version !== 'v1' || !sig) return false;
  const bytes = Buffer.from(sig, 'base64');
  return bytes.length === expected.length && crypto.timingSafeEqual(bytes, expected);
});

```

Verify the **raw body**, return `400` on failure, and optionally reject timestamps more than 5 minutes old (the examples do; Recall.ai's sample doesn't). There's no Recall.ai SDK for this; `standardwebhooks` or `svix` libraries also work.

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

## Common Event Types

The event name is in the body's top-level `event` field (not `type`).

| Event | Delivery | Description |
| --- | --- | --- |
| `bot.joining_call` | Svix | Bot is connecting to the meeting |
| `bot.in_waiting_room` | Svix | Bot is in the waiting room |
| `bot.in_call_not_recording` | Svix | Bot joined but isn't recording |
| `bot.in_call_recording` | Svix | Bot is recording |
| `bot.call_ended` | Svix | Bot left the call; media may not be ready yet |
| `bot.done` | Svix | Bot shut down; recorded media is available |
| `bot.fatal` | Svix | Bot failed (`data.data.sub_code` has the reason) |
| `recording.done` / `recording.failed` | Svix | Recording artifact finished or failed |
| `transcript.done` / `transcript.failed` | Svix | Transcript artifact finished or failed |
| `calendar.update` | Svix | Calendar changed (e.g. disconnected); re-fetch it |
| `calendar.sync_events` | Svix | Calendar events changed since `data.last_updated_ts` |
| `participant_events.join` / `participant_events.leave` | Real-time | Participant joined or left |
| `transcript.data` | Real-time | Transcript utterance generated during the call |

> **For the full list** (media artifacts like `audio_mixed.done`, breakout room events, all participant events), see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recall-ai-webhooks/references/overview.md).

## Environment Variables

```bash
# Workspace verification secret, or the legacy Svix endpoint signing secret
RECALL_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

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

```

Add the tunnel URL as an endpoint in your region's Recall.ai Webhooks dashboard (us-east-1, us-west-2, eu-central-1 or ap-northeast-1), or as a `realtime_endpoints` URL on Create Bot.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recall-ai-webhooks/references/overview.md) - Svix vs. real-time webhooks, full event list, payloads, retry behaviour
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recall-ai-webhooks/references/setup.md) - Regional dashboards, which secret to use, endpoint and real-time configuration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recall-ai-webhooks/references/verification.md) - Signature verification details, gotchas and debugging