---
title: Recall.ai
description: Put your agent in Zoom, Google Meet, and Microsoft Teams meetings through Recall.ai meeting bots.
url: "https://eve.dev/docs/channels/recall"
docs_index: /llms.txt
type: integration
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

> **Experimental:**
>
> The Recall.ai channel is experimental. Its API and defaults may change in a patch release.

The Recall.ai channel puts your agent in a video meeting as a bot. [Recall.ai](https://www.recall.ai) runs the bot in Zoom, Google Meet, or Microsoft Teams. It sends the meeting's live transcript and chat to your agent as webhooks, and the agent replies in the meeting chat. Each bot's meeting is one eve session, and each participant is their own principal. See [Channels](/docs/channels) for the contract this builds on.

## Add the channel

```bash
eve add channel/recall
```

This installs the `@eve/recall` package and writes `agent/channels/recall.ts`:

```ts title="agent/channels/recall.ts"
import { recallChannel } from "@eve/recall";

export default recallChannel();
```

```bash
RECALL_API_KEY=...                         # required to create bots and post to chat
RECALL_WORKSPACE_VERIFICATION_SECRET=whsec_... # required to verify webhooks
RECALL_REGION=us-east-1                    # optional; your Recall workspace region
RECALL_WEBHOOK_URL=https://...             # local development only; see below
```

Create the API key in the Recall dashboard, and copy the workspace verification secret from **Developers > Webhooks**. To skip env vars, pass the same values through `credentials: { apiKey, verificationSecret }` and `region`.

The channel mounts one route, `POST /eve/v1/recall`, for both real-time and bot status webhooks. Set `route` to change it. Recall must reach this route over public HTTPS, so for local development, expose `eve dev` through a tunnel.

## Send a bot to a meeting

Recall sends a bot's real-time speech and chat events to the webhook URL set when the bot is created, so every bot must be created with the channel's route. There are two ways to create one.

### From eve

Hand a request to the channel with its `{ meetingUrl, metadata? }` target. The channel creates the bot and starts the session with your message. This example runs from a schedule; a route handler uses the same target through `ctx.to(...)`.

```ts title="agent/schedules/standup.ts"
import { defineSchedule } from "eve/schedules";

import recall from "../channels/recall";

export default defineSchedule({
  cron: "0 9 * * 1-5",
  async run({ to, waitUntil, appAuth }) {
    waitUntil(
      to(recall, { meetingUrl: "https://zoom.us/j/1234567890" }).send(
        "Join the standup and take notes.",
        { auth: appAuth },
      ),
    );
  },
});
```

Bots created this way send webhooks to `webhookUrl`. It defaults to `RECALL_WEBHOOK_URL`, then to the channel's route on the current Vercel deployment: the production URL in production, and the deployment's own URL on previews, so a preview's bots report to the preview. The default includes the agent's route prefix when the agent is mounted under one. It never includes a [Deployment Protection](https://vercel.com/docs/deployment-protection) bypass secret, so on a protected deployment, set `RECALL_WEBHOOK_URL` to a URL Recall can reach. Outside Vercel, such as in local development through a tunnel, set `RECALL_WEBHOOK_URL`.

A new bot can't post to the chat until the meeting admits it, which can take minutes when the meeting has a waiting room. Until then, the channel holds the agent's chat messages and posts them once the session receives its first meeting event, such as someone speaking or joining. If nobody speaks, chats, or joins after the bot is admitted, the held messages wait until someone does. While a message is held, `sendChatMessage` returns `true`, and a later failure to post it is only logged.

### From your own code

To create bots elsewhere, such as in a tool or for Recall's calendar auto-join, build the request body with `recallBotConfig` and send it with `createRecallBot`. The config subscribes the bot to every real-time event the channel understands and turns on Recall's low-latency streaming transcription.

```ts
import { createRecallBot, recallBotConfig, resolveRecallWebhookUrl } from "@eve/recall";

const bot = await createRecallBot({
  body: recallBotConfig({
    meetingUrl: "https://meet.google.com/abc-defg-hij",
    webhookUrl: resolveRecallWebhookUrl(),
    greeting: "Hi, I'm taking notes. Type in the chat or say my name to ask me something.",
  }),
});
```

`recallBotConfig` returns a plain object. Spread it into your own body for Recall options eve doesn't model. Recall's low-latency transcription is English-only; replace `recording_config.transcript` with a third-party streaming provider for other languages.

## How the channel handles meetings

### What the agent hears

Two hooks decide what reaches the agent:

- `onMeetingEvent(ctx, event)` runs in the webhook route and decides which events enter the session and as whom. Return `{ auth }` to deliver the event, optionally with `message` to replace the line the model sees, or `null` to drop it. The default delivers speech, chat, joins, and leaves, each as its participant, and drops bot status changes.
- `shouldRespond({ event, line, transcript, botName })` runs in the session and decides whether a delivered line starts a turn. The default answers every chat message and any speech that names the bot. A throw falls back to the default.

A line that `shouldRespond` declines costs no model call. The channel keeps the last 60 declined lines and gives them to the next turn as recent transcript, so the agent knows what "can you file that?" refers to. A declined line also doesn't cancel a pending approval or question.

The model sees speech as `Alice: ...` and chat as `Alice (in chat):` followed by the message. Each turn also gets the bot id and the speaker's participant id, name, and host flag.

`shouldRespond` is a good place for a fast decision model, because it sees the recent transcript:

```ts title="agent/channels/recall.ts"
import { decide } from "eve/ai";
import { recallChannel } from "@eve/recall";

export default recallChannel({
  async shouldRespond({ event, line, transcript, botName }) {
    if (event.type === "chat") return true;
    if (event.type !== "speech") return false;
    const { answers } = await decide({
      state: { assistant: botName, recentTranscript: transcript.slice(-10), latest: line },
      questions: {
        addressed: {
          type: "boolean",
          instructions: `Is the latest line asking ${botName} to do or answer something?`,
        },
      },
      abortSignal: AbortSignal.timeout(5_000),
    });
    return answers.addressed.probability >= 0.5;
  },
});
```

### Identity

Each participant is the principal `recall:<botId>:<participantId>` with `principalType: "user"`. Participant ids are scoped to one bot, so nothing carries over between meetings. Display names, host flags, and emails come from the meeting platform, and anyone can set their display name, so don't use them for authorization.

To treat the whole meeting as one principal instead, return `recallMeetingAuth(event)` from `onMeetingEvent`. With one principal, the `queue` turn policy can batch a burst of lines from different speakers into one turn.

```ts
import { recallChannel, recallMeetingAuth } from "@eve/recall";

export default recallChannel({
  onMeetingEvent: (_ctx, event) =>
    event.type === "status" ? null : { auth: recallMeetingAuth(event) },
});
```

`botName` sets the bot's display name, which defaults to `eve`. The channel matches it to find speech addressed to the bot. To ignore the bot's own chat messages, the channel drops chat under `botName`, under the name Recall reports for the bot, and chat that repeats one of the bot's last 10 messages word for word. A participant with the same display name as the bot is ignored too. Pass the same `botName` to `recallBotConfig` for bots you create yourself.

### Replies

The default `content.completed` handler posts each reply to the meeting chat, including text the agent writes before a tool call. Long replies are split at the platform's limit: 500 characters on Google Meet, and 4,096 on Zoom and Microsoft Teams.

The bot doesn't take private chat messages. Everyone in the meeting shares one session, so anything the agent reads stays in history that any participant can ask about later. When someone sends the bot a private Zoom chat message, the bot replies privately that it can't take private messages, and the message never reaches the session or `onMeetingEvent`.

Webex bots can't send chat messages. The channel replies only in text; it doesn't speak.

### Overlapping messages

The channel defaults to `turnPolicy: "queue"`, because people keep talking while the agent works, and `"steer"` would restart the turn on every utterance. Lines spoken during a turn are delivered after it ends, and each runs `shouldRespond` then.

While a turn waits on a tool approval or sign-in, the person it's working for can still redirect it: a message from them that doesn't answer the prompt cancels the prompt and steers the turn. A question asked with `ctx.ask()` inside a [workflow tool](/docs/tools/workflows) doesn't work this way yet. Until someone answers it, every message waits behind it and the bot stays quiet. If that matters for your agent, set `turnPolicy: "steer"`, which lets a message cancel the question but interrupts pending model output on every line the session receives.

### Human-in-the-loop (HITL)

Meeting chat has no buttons, so the default handlers post each question and tool approval as text with numbered options. Participants answer in the chat, or by speaking to the bot. Answers can be phrased naturally: "okay eve, approve it" approves, and "eve, option two" or "the second one" picks the second option. The bot's name, filler words, and number words are ignored, and what's left must name exactly one option, so "neither, approve is wrong" doesn't pick one. To render questions differently, declare your own `events["interaction.opened"]` handler.

### Sign-ins

A sign-in link or code is a credential, so it never goes to the shared chat. On Zoom, the bot sends it privately to the participant who needs to sign in. On other platforms, the bot asks them to connect the account outside the meeting. Instructions without a link or code, such as "approve on your phone", are posted for everyone.

### Meeting end

Recall sends bot status changes, such as `call_ended`, as workspace webhooks that you configure in the Recall dashboard. Point them at the same route to receive them. The default `onMeetingEvent` drops them. Return `{ auth }` for a status event to deliver it, for example to have the agent post a summary when the call ends.

## Recall API handle

Hooks and event handlers receive `recall`, a handle on the meeting's bot:

- `sendChatMessage(message, { to? })` posts to the chat and returns `false` if Recall rejects it. Pass a participant id as `to` to reach one person; check `supportsDirectMessages` first. The channel learns the meeting platform from the bot itself, so `platform` and `supportsDirectMessages` are only known inside the session, not in `onMeetingEvent`.
- `outputAudio(mp3)` plays an MP3 clip through the bot. The bot must be created with Recall's `automatic_audio_output` option, and you can't combine it with Output Media.
- `startOutputMedia(url)` and `stopOutputMedia()` stream a web page into the meeting as the bot's camera and microphone, using Recall Output Media. Output Media isn't available in Slack huddles.
- `leaveCall()` removes the bot from the meeting.
- `request(path, init)` calls any Recall REST endpoint under `/api/v1`.

The same operations are exported as standalone functions for code outside the channel, such as `sendRecallChatMessage`, `leaveRecallCall`, and `callRecallApi`.

## Webhook handling

The route answers `503` when no verification secret is configured and `413` for bodies over 256 KiB. It then checks Recall's `webhook-signature` header and answers `401` when the signature doesn't match. Nothing reaches the session before the signature check. Recall retries any other non-2xx response every second, so the route acknowledges with `204` before it delivers the event, and also acknowledges signed bodies it can't parse.

## Disclaimer

As the deployer, it is your responsibility to ensure your agent complies with applicable laws.

For example, you may be required to tell meeting participants that the meeting is transcribed and processed by an automated AI system, and to obtain consent where required, including in two-party-consent jurisdictions. A `greeting` in `recallBotConfig` is one way to announce the bot when it joins.

## What to read next

- [Channels overview](/docs/channels): the channel contract and every built-in channel
- [Auth & route protection](/docs/guides/auth-and-route-protection): principals and authorization
- [Custom channels](/docs/channels/custom#cross-channel-hand-off): hand a request from another channel into a meeting

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)