Gareth Wilson Gareth Wilson

Guide to Recall.ai Webhooks: Features and Best Practices

Published


Recall.ai webhooks tell your app what your meeting bots and recordings are doing: a bot is joining a call, waiting to be let in, recording, or done; a recording or transcript has finished processing; a user's calendar has changed; or a desktop recording has uploaded. During a call, real-time endpoints can also stream participant events and transcript data to you. If you build note-takers, sales tools or meeting analytics on Recall.ai, webhooks are how your app follows each bot from start to finish.

This guide covers the two webhook systems Recall.ai uses, how requests are signed and how that depends on when your account was created, the main events, the retry and timeout rules, and the best practices for running a Recall.ai receiver in production.

What are Recall.ai webhooks?

Recall.ai sends webhooks in two ways.

Svix webhooks carry status changes: bots, recordings, transcripts and other media artifacts, Calendar V2 and Desktop SDK uploads. You add endpoints and subscribe to event types in the webhooks dashboard for your region. Recall.ai only sends the event types you subscribe to.

Real-time endpoints carry in-call data such as participant joins, chat messages and transcript segments. You set them on each bot when you create it. They don't appear in the webhooks dashboard and follow different retry rules.

Status webhooks share one shape: an event name and a data object holding the status (data.data.code, sub_code and updated_at) and the object it applies to, such as data.bot.

Signing depends on your account. Once you create a workspace verification secret, every request carries webhook-id, webhook-timestamp and webhook-signature. Until you do, those headers are not sent. Accounts created before 15 December 2025 sign Svix webhooks with each endpoint's own Svix secret and the svix-* headers instead.

Recall.ai webhook features

FeatureDetails
Status webhooksSvix, configured in the regional webhooks dashboard
Real-time dataReal-time endpoints, set per bot
Regionsus-east-1, us-west-2, eu-central-1, ap-northeast-1, each with its own dashboard
Event selectionPer endpoint. Unsubscribed events are not sent
Signature headerswebhook-id, webhook-timestamp, webhook-signature (or svix-* on legacy accounts)
SchemeHMAC-SHA256, base64, over {id}.{timestamp}.{raw body}
SecretWorkspace verification secret, whsec_ prefixed, opt-in. Legacy: per-endpoint Svix secret
RotationThe old secret stays valid for 24 hours. At most 9 active secrets
Event nameTop-level event field
Timeout15 seconds. It can't be increased
Svix retriesImmediately, then 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours
Svix recoveryRecover failed messages for a time window in the dashboard
Auto-disableA Svix endpoint failing for 5 days is disabled. Email alerts are available
Real-time retries60 attempts at 1-second intervals, then the endpoint is marked failed. No replay
Source IPsSvix webhooks come from static IPs. Real-time endpoints don't
Query parametersPassed through on both kinds of URL

Common events

Bot status events

EventMeaning
bot.joining_callThe bot is connecting. Can be sent more than once if a join is retried
bot.in_waiting_roomThe bot is in the meeting's waiting room
bot.in_call_not_recordingThe bot has joined but isn't recording
bot.recording_permission_allowed / bot.recording_permission_deniedThe host allowed or denied recording. See sub_code for the reason
bot.in_call_recordingThe bot is recording
bot.call_endedThe bot left the call. Media may not be ready yet
bot.doneThe bot has shut down, and any recording is uploaded
bot.fatalAn error shut the bot down, usually when it couldn't join. See sub_code

There are also bot.breakout_room_* events for breakout rooms. A bot status webhook looks like this:

{
  "event": "bot.done",
  "data": {
    "data": {
      "code": "done",
      "sub_code": null,
      "updated_at": "2026-10-21T09:41:12.000000Z"
    },
    "bot": {
      "id": "6c8b2f7a-1d2e-4f3a-9b8c-7d6e5f4a3b2c",
      "metadata": {}
    }
  }
}

Recording, transcript and media events

Recordings and each media artifact (transcript, participant_events, video_mixed, video_separate, audio_mixed, audio_separate and meeting_metadata) have processing, done, failed and deleted events, such as recording.done and transcript.failed.

Calendar and Desktop SDK events

Calendar V2 sends calendar.update when a calendar's data changes, such as its status becoming disconnected, and calendar.sync_events when events on a calendar are created, updated or deleted. The Desktop SDK sends sdk_upload.recording_started, sdk_upload.recording_ended, sdk_upload.complete and sdk_upload.failed:

{
  "event": "sdk_upload.complete",
  "data": {
    "data": {
      "code": "complete",
      "sub_code": null,
      "updated_at": "2025-05-10T15:48:24.034072Z"
    },
    "recording": { "id": "0206f11b-68ab-42b1-8a30-b85f60076e83", "metadata": {} },
    "sdk_upload": { "id": "fa08e580-d7b6-4f88-9f71-5b718c5a5f77", "metadata": {} }
  }
}

Real-time events

Real-time endpoints receive events such as participant_events.join, participant_events.leave, participant_events.speech_on, participant_events.chat_message, transcript.data and transcript.partial_data.

Setting up Recall.ai webhooks

  1. On the API keys page of your region's dashboard, create a workspace verification secret. It starts with whsec_. Store it server-side, for example as RECALL_WEBHOOK_SECRET. If your account predates 15 December 2025, copy each endpoint's Svix signing secret from the webhooks dashboard instead.
  2. In the webhooks dashboard for your region, add an endpoint with your HTTPS URL.
  3. Subscribe the endpoint to every event type you want. Events you don't subscribe to are not sent.
  4. For real-time data, add a real-time endpoint when you create each bot.

You can add query parameters to a webhook URL, such as ?env=staging, and Recall.ai passes them through, which helps when several environments share one receiver.

Securing Recall.ai webhooks

Build the signed content as {webhook-id}.{webhook-timestamp}.{raw body}, compute an HMAC-SHA256 with the base64-decoded part of your secret after whsec_, base64-encode it, and compare it in constant time with each v1, entry in webhook-signature. Recall.ai's own verification function falls back to the svix-* headers, so one function covers both kinds of account.

const crypto = require('crypto');
const express = require('express');

function verifyRecallSignature(rawBody, headers, secret) {
  const id = headers['webhook-id'] ?? headers['svix-id'];
  const timestamp = headers['webhook-timestamp'] ?? headers['svix-timestamp'];
  const signatureHeader = headers['webhook-signature'] ?? headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader) return false;
  if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;

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

  // During a rotation the header holds one "v1,<base64>" entry per active secret.
  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/recall', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.RECALL_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyRecallSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  // Optional: if your own policy needs it, reject timestamps that are too old.
  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event, then process async. Respond well inside 15 seconds.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_recall_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("webhook-id") or headers.get("svix-id")
    timestamp = headers.get("webhook-timestamp") or headers.get("svix-timestamp")
    signature_header = headers.get("webhook-signature") or headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header):
        return False
    if not isinstance(secret, str) or not secret.startswith("whsec_"):
        return False

    key = base64.b64decode(secret[len("whsec_"):])
    signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode(), expected):
            return True
    return False

To check your verifier, use the secret whsec_aG9va2RlY2stZ3VpZGUtdGVzdC1rZXktMzJieXRlcyE=, the headers webhook-id: msg_2kRecallTest0001 and webhook-timestamp: 1761040800, and this exact body:

{"event":"bot.done","data":{"bot":{"id":"6c8b2f7a-1d2e-4f3a-9b8c-7d6e5f4a3b2c","metadata":{}}}}

The webhook-signature header should be:

v1,2iHMjJ7i8bqfujiWPY8x+VV2JMmCoxLDU9JDZqj+dUw=

If you haven't created a workspace secret, new accounts receive no signature headers, so there is nothing to verify. Create one before you go to production. In Hookdeck, leave verification off if you didn't set a secret. Real-time webhooks from the Desktop SDK can't be verified at all. For real-time endpoints, Recall.ai also supports a ?token= query parameter, which needs a trailing / before the query string or requests fail with 400.

Make Recall.ai webhooks production-ready. Hookdeck Event Gateway verifies the webhook-signature (or svix-signature) on every Recall.ai delivery, responds well inside the 15-second timeout, and queues bot and recording events durably before your service sees them.

Recall.ai webhook limitations and pain points

Signing is opt-in

The Problem: Your endpoint accepts forged bot events, because nothing on the request proves it came from Recall.ai.

Why It Happens: On accounts created from 15 December 2025, the webhook-* headers are only sent after you create a workspace verification secret.

Workarounds:

  • Create a workspace secret before you build your receiver, and reject requests without signature headers.
  • If you use the ?token= approach on real-time endpoints, keep the token long and random.

How Hookdeck Can Help: Hookdeck's Recall.ai source verifies the signature with your workspace secret and accepts both the webhook-* and svix-* header sets, so legacy endpoints work too.

A 15-second timeout you can't change

The Problem: A handler that downloads media or calls an LLM before responding times out, and Recall.ai retries an event you already processed.

Why It Happens: Status webhooks time out after 15 seconds, and Recall.ai says the timeout can't be increased. A timed-out request counts as failed "even if the work has completed".

Workarounds:

  • Verify, store the event, and return 200 straight away. Do the work in a background job.
  • Make processing idempotent so a retry after a slow success does nothing.

How Hookdeck Can Help: Hookdeck acknowledges each webhook as soon as it arrives, then delivers it to your service with the timeout and retry policy you choose.

Real-time endpoints fail fast and can't be replayed

The Problem: A short outage during a meeting loses live transcript data, and the endpoint stops receiving anything for the rest of the call.

Why It Happens: Real-time endpoints retry 60 times at 1-second intervals. After that, the endpoint is marked failed and no more messages are delivered. There is no manual retry in the dashboard or API.

Workarounds:

  • Keep the real-time receiver as small as possible and push work to a queue.
  • After the meeting, fetch the transcript and participant data asynchronously while the recording media is still available.

How Hookdeck Can Help: Hookdeck accepts each real-time request immediately, so a brief outage on your side doesn't use up Recall.ai's 60 retries. Events wait in Hookdeck until your service is back.

Media isn't ready at bot.call_ended

The Problem: Your app tries to download a recording and gets nothing.

Why It Happens: bot.call_ended means the bot left the call. Recall.ai notes recordings "may not be available at this point".

Workarounds:

  • Wait for bot.done before downloading media, or for the artifact's own *.done event, such as transcript.done.
  • Make sure the endpoint is subscribed to those events, because unsubscribed events aren't sent.

How Hookdeck Can Help: Hookdeck filters can route bot.done and *.done events to the service that downloads media, and keep status updates elsewhere.

Best practices

Rotate secrets without downtime

After you rotate the workspace secret, the old one stays valid for 24 hours and each request carries a signature for every active secret. Accept any matching entry, update your stored secret, and treat multiple signatures as a sign that a rotation is in progress.

Subscribe deliberately

Only subscribed event types are sent. Review your subscriptions when you start using a new feature, such as Calendar V2 or the Desktop SDK.

Expect repeats

bot.joining_call can be sent more than once for the same bot, and Svix retries resend a message with the same ID. Deduplicate on the webhook-id (or svix-id) header, and make state transitions idempotent.

Use query parameters to tell environments apart

Add a parameter such as ?env=staging to each workspace's webhook URL. It comes back on every request.

Acknowledge fast and process asynchronously

Return 200 well inside the 15-second limit and queue the work. See why to process webhooks asynchronously.

Make handlers idempotent

Processing the same event twice should have the same result as processing it once. See our guide to webhook idempotency.

Conclusion

Recall.ai sends status webhooks through Svix and in-call data through real-time endpoints, both signed with an HMAC-SHA256 over the ID, timestamp and raw body once you create a workspace verification secret. Create that secret, accept both webhook-* and svix-* headers if you have older endpoints, respond within 15 seconds, and wait for bot.done before you fetch media.

Hookdeck Event Gateway verifies every Recall.ai signature, answers inside the timeout, and holds each event until your service has processed it, so an outage on your side doesn't cost you a meeting's worth of data.

Get started with Hookdeck for free and handle Recall.ai webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

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