> ## Documentation Index
> Fetch the complete documentation index at: https://anam.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Meetings

> Invite a persona to join a Google Meet, Zoom, or Microsoft Teams call as a participant.

Personas can join your video meetings. You create an invite with a meeting URL and a persona, and the persona appears in the call like any other participant: it shows up in the roster under its display name, listens to the conversation, and speaks with its avatar on camera. Everyone in the meeting can talk to it.

This works with Google Meet, Zoom, and Microsoft Teams. Use it for AI-led user interviews, training and roleplay calls, product demos, or a subject-matter expert your team can pull into a call and question.

Meetings are driven by the [Meetings API](/api-reference/meetings/create-meeting-invite), which requires an API key with the `meetings` permission scope. You can also invite a published persona from its Build page in the [Lab](https://lab.anam.ai). The API adds scheduling and region control on top.

<Info>
  Meeting participants always see that the persona is an AI. Its display name carries an "(AI)" suffix, added server-side if the name you provide doesn't already include one, and its video shows a persistent AI-disclosure treatment.
</Info>

## Invite a persona

Send a meeting URL and a persona to `POST /v1/meetings/invites`:

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.anam.ai/v1/meetings/invites \
    -H "Authorization: Bearer $ANAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "meetingUrl": "https://meet.google.com/abc-defg-hij",
      "displayName": "Cara",
      "personaId": "00000000-0000-0000-0000-000000000000"
    }'
  ```

  ```typescript Node.js theme={"system"}
  const response = await fetch("https://api.anam.ai/v1/meetings/invites", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ANAM_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      meetingUrl: "https://meet.google.com/abc-defg-hij",
      displayName: "Cara",
      personaId: "00000000-0000-0000-0000-000000000000",
    }),
  });

  const invite = await response.json();
  ```
</CodeGroup>

Provide exactly one of:

* `personaId` — a saved persona from the Lab or the [Personas API](/api-reference/personas/create-persona).
* `personaConfig` — a runtime configuration, the same shape you use when [creating a session token](/api-reference/sessions/create-session-token). It needs at least an `avatarId`, a `voiceId`, and an `llmId`. The configuration is used for the meeting session but not stored.

The response is the invite in `pending` status. Without a `joinAt`, the persona starts joining right away.

```json theme={"system"}
{
  "id": "00000000-0000-0000-0000-000000000000",
  "provider": "google_meet",
  "meetingUrl": "https://meet.google.com/abc-defg-hij",
  "displayName": "Cara (AI)",
  "status": "pending",
  "createdAt": "2026-07-10T10:00:00.000Z",
  "joinAt": null,
  "groupCall": true,
  "sessionId": null,
  "region": "eu",
  "joinState": null,
  "statusReason": null
}
```

If the meeting has a waiting room, someone in the call needs to admit the persona, which appears under its display name. While it waits, the invite's `joinState` is `waiting_room`. If nobody lets it in, the invite ends as `failed` with a `statusReason` of `not_admitted` or `join_denied`.

## Schedule the join

For a meeting you know about in advance, set `joinAt` instead of creating the invite at meeting time:

```json theme={"system"}
{
  "meetingUrl": "https://meet.google.com/abc-defg-hij",
  "displayName": "Cara",
  "personaId": "00000000-0000-0000-0000-000000000000",
  "joinAt": "2026-07-14T15:00:00Z"
}
```

Scheduling at least 10 minutes ahead reserves guaranteed capacity, so the persona's spot on the call is held for it. Immediate joins, and `joinAt` times less than 10 minutes out, use on-demand capacity instead and can be rejected with a `503` at busy moments.

`joinAt` is ISO 8601 and must include a timezone (`Z` or an offset such as `+02:00`). It must be in the future, at most 7 days ahead. The invite stays `pending` until the scheduled time, and you can cancel it at any point before then.

<Tip>
  When a calendar integration or booking flow tells you the meeting time, create the invite as soon as the meeting is scheduled rather than when it starts. Guaranteed capacity is the most reliable way to get a persona into a planned meeting.
</Tip>

## Group calls and 1:1 calls

By default the persona behaves as a well-mannered participant in a multi-person meeting: it joins silently and speaks only when addressed by its display name ("Cara, what do you think?"). This is the `groupCall: true` default.

Set `groupCall: false` when the persona is the main counterpart on the call, such as an interview or a 1:1 demo. It then greets everyone when it joins and responds to everything it hears, the same way it would in a regular session.

## Follow the invite

An invite moves through a small set of states. Poll `GET /v1/meetings/invites/{id}` to follow it, or filter the list endpoint to watch several at once:

```bash theme={"system"}
curl "https://api.anam.ai/v1/meetings/invites?status=active" \
  -H "Authorization: Bearer $ANAM_API_KEY"
```

| Status      | Meaning                                                                 |
| ----------- | ----------------------------------------------------------------------- |
| `pending`   | Created; waiting for a scheduled `joinAt` or in the process of joining. |
| `active`    | The persona is in the call and its session is running.                  |
| `ended`     | The call finished normally: the meeting ended or the persona left.      |
| `cancelled` | You cancelled the invite.                                               |
| `failed`    | The persona couldn't join, or the call ended abnormally.                |

Two fields add detail:

* `joinState` reports the persona's progress into the call as it happens: `joining`, `waiting_room`, `in_call`, `media_denied`, `media_granted`, `media_active`, `left`, `done`, or `error`. New values may appear over time, so treat unrecognized ones as informational rather than as errors.
* `statusReason` explains how a finished invite got there, for example `not_admitted`, `removed_by_host`, `meeting_ended`, `meeting_link_invalid`, or `idle_timeout`. It's `null` while the invite is in progress, and may also gain new values over time.

Once the invite is `active`, its `sessionId` refers to an ordinary session. Everything in the [Sessions API](/api-reference/sessions/get-session) works with it, including fetching the transcript after the call.

## Remove the persona

`DELETE /v1/meetings/invites/{id}` cancels a pending invite. If the persona has already joined, it leaves the call and its session ends immediately:

```bash theme={"system"}
curl -X DELETE https://api.anam.ai/v1/meetings/invites/$INVITE_ID \
  -H "Authorization: Bearer $ANAM_API_KEY"
```

The call returns `204` and is idempotent for invites that already finished. You don't have to clean up invites you leave alone: the persona leaves on its own when the meeting ends.

## Regions

The persona joins the call from one of three regions: `eu`, `us-east`, or `us-west`. If you omit `region`, one is picked automatically based on where your request comes from. For the lowest latency, set the region closest to the meeting's participants, which may not be where your server runs.

`regionPolicy` controls what happens when your chosen region is out of capacity:

* `preferred` (the default) may serve the invite from another region so the persona still joins.
* `strict` never leaves the selected region and fails with a `503` instead. It requires an explicit `region`.

Use `strict` when data residency matters, for example to keep an EU customer's call in the EU:

```json theme={"system"}
{
  "meetingUrl": "https://meet.google.com/abc-defg-hij",
  "personaId": "00000000-0000-0000-0000-000000000000",
  "region": "eu",
  "regionPolicy": "strict"
}
```

The `region` field on the invite is the region actually serving the call, which under `preferred` can differ from the one you asked for.

## Limits and capacity

Invite creation is rate limited to 10 requests per minute per requester and 30 per hour per organization. Successful (`201`) and rate-limited (`429`) responses both carry `RateLimit-*` headers for the per-minute window and `X-RateLimit-Org-Hour-*` headers for the hourly one.

A `503` on create means there's no on-demand capacity right now. The response includes a `Retry-After` header; retry after that many seconds, or switch to a scheduled `joinAt` at least 10 minutes ahead so capacity is reserved instead. A `422` means the meeting service rejected the invite itself, which usually points to a problem with the meeting URL.

The full request and response reference, including every error shape, is in the [Meetings API reference](/api-reference/meetings/create-meeting-invite).
