> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# MeetStream Guide: Outlook Calendar Integration & Auto-Scheduling

This guide explains how to connect Microsoft Outlook / Microsoft 365 Calendar accounts to MeetStream so bots can automatically join your meetings — no manual API calls required.

Applies to: **Google Meet, Zoom, Microsoft Teams** meetings on your Outlook Calendar.
Support: docs.meetstream.ai | API: api.meetstream.ai

---

## What you get with Calendar Integration

Once connected, MeetStream can:

- **Connect many Microsoft accounts per user** — one MeetStream user can attach a personal Outlook.com account, a work Microsoft 365 tenant, a contractor account in a third tenant, and so on. Up to 200 per user (Google + Outlook combined).
- **See upcoming meetings across every connected account** — synced directly from Microsoft Graph with per-account delta tokens for speed.
- **Schedule bots for specific meetings** — one click or one API call.
- **Auto-schedule bots for all meetings** — hands-free, every day, across every connected account.
- **Handle recurring meetings** — automatically reschedule bots for the next occurrence.
- **React to calendar changes in real time** — if a meeting is rescheduled, the bot's join time updates automatically; if a meeting is cancelled, the bot is cancelled too. Notifications are routed per-account so one tenant's churn never disturbs another's.
- **Manage scheduled bots** — list, update, or delete scheduled bots at any time.
- **Disconnect one account at a time** — remove a single Microsoft account without touching the others.
- **Keep subscriptions alive** — Microsoft Graph change notification subscriptions are automatically renewed before they expire.

> **Mental model.** A MeetStream user owns a set of **calendar connections**. Each connection is uniquely identified by a `(provider, account_id)` pair — for Outlook, `account_id` is the account's primary email (e.g. `jane@acme.com`) as returned by Microsoft Graph's `me` endpoint. Every endpoint in this guide either operates across **all** of a MeetStream user's connections (the default) or scopes to a single connection via `?provider=&account_id=` query parameters or a path parameter. The provider key is `outlook` everywhere; the alias `microsoft` is accepted too. If you only ever connect one Outlook account per MeetStream user, the multi-account surface is invisible to you — everything just works.

---

## 1) Get your Microsoft OAuth credentials

Before connecting your calendar, you need three things from Microsoft: a **Client ID**, **Client Secret**, and **Refresh Token**. Follow these steps to get them.

### Step 1: Register an app in Azure Portal

1. Go to the [Azure Portal](https://portal.azure.com/).
2. Navigate to **Azure Active Directory > App registrations**.
3. Click **New registration**.
4. Give your app a name (e.g. "MeetStream Calendar").
5. Under **Supported account types**, select **Accounts in any organizational directory and personal Microsoft accounts**.
6. Under **Redirect URI**, select **Web** and enter:
   ```
   http://localhost:3000/api/microsoft/oauth-callback
   ```
7. Click **Register**.
8. On the app overview page, copy your **Application (client) ID** — this is your **Client ID**.

### Step 2: Create a client secret

1. In your app registration, go to **Certificates & secrets**.
2. Click **New client secret**.
3. Give it a description and choose an expiry period.
4. Click **Add** and immediately copy the **Value** — this is your **Client Secret**. It won't be shown again.

### Step 3: Add Microsoft Graph API permissions

1. In your app registration, go to **API permissions**.
2. Click **Add a permission > Microsoft Graph > Delegated permissions**.
3. Add the following permissions:

| Permission | Purpose |
|---|---|
| `Calendars.Read` | Read calendar events |
| `User.Read` | Read user profile and email |
| `offline_access` | Obtain refresh tokens for long-lived access |

4. Click **Grant admin consent** if you have admin rights, or ask your tenant admin to grant consent.

### Step 4: Run the OAuth helper to get your refresh token

MeetStream provides a lightweight Node.js helper that runs the OAuth consent flow locally and returns your refresh token.

**Prerequisites:** Node.js installed on your machine.

1. Create a `.env` file in the project root with your credentials:

```
MICROSOFT_CLIENT_ID=<YOUR_CLIENT_ID>
MICROSOFT_CLIENT_SECRET=<YOUR_CLIENT_SECRET>
```

2. Install dependencies and start the helper server:

```bash
npm install express @azure/msal-node dotenv
node server_microsoft.js
```

You'll see:

```
Open http://localhost:3000 to start the OAuth flow
Redirect URI: http://localhost:3000/api/microsoft/oauth-callback

Make sure this redirect URI is added in Azure Portal → App registrations → Authentication → Redirect URIs
```

3. Open **http://localhost:3000** in your browser.
4. Sign in with your Microsoft account and grant calendar access.
5. After authorization, the page displays your **Refresh Token** and **Access Token**.
6. Copy the **Refresh Token** — you'll need it in the next step.

> If the refresh token is not returned, make sure `offline_access` is included in your permissions and that you clicked **Accept** on the consent screen. Microsoft only returns a refresh token when `offline_access` is in the requested scope.

### Required scopes

The helper requests these scopes:

| Scope | Purpose |
|---|---|
| `Calendars.Read` | Read calendar events and calendar list |
| `User.Read` | Identify the Microsoft account and profile |
| `offline_access` | Obtain long-lived refresh tokens |

You now have everything you need: **Client ID**, **Client Secret**, and **Refresh Token**.

---

## 2) Connect an Outlook Calendar account

Each call to this endpoint connects **one** Microsoft account. To attach a second, third, or twentieth account to the same MeetStream user, call it again with a different refresh token. MeetStream resolves the account from Microsoft Graph's `me` endpoint and stores each connection separately.

### API endpoint

```
POST https://api.meetstream.ai/api/v1/calendar/create_outlook_calendar
```

### Request body

```json
{
  "microsoft_client_id": "<YOUR_MICROSOFT_CLIENT_ID>",
  "microsoft_client_secret": "<YOUR_MICROSOFT_CLIENT_SECRET>",
  "microsoft_refresh_token": "<YOUR_MICROSOFT_REFRESH_TOKEN>"
}
```

Optional:

| Field | Type | Default | Description |
|---|---|---|---|
| `replace` | bool | `false` | When `true`, allows the call to overwrite an existing connection for the same `account_id` (token rotation, scope refresh). Without this flag, reconnecting an already-connected account returns **409 Conflict**. |

### What happens behind the scenes

1. MeetStream exchanges the refresh token at Microsoft's OAuth endpoint to validate the credentials.
2. Calls Microsoft Graph's `me` endpoint to resolve the account's primary email — that email becomes the `account_id` for this connection.
3. Fetches all calendars on that account (default, secondary, shared) via Microsoft Graph.
4. Stores your credentials securely (encrypted at rest in AWS SSM Parameter Store) under a per-account path, isolated from any other accounts you've connected.
5. Registers **Microsoft Graph change notification subscriptions** on every calendar on that account, pointing at a per-account webhook URL with a per-subscription `clientState` secret so notifications for this account never cross-contaminate any other connection on the same user.
6. Returns the connection record (no token material).

### Example cURL

```bash
curl -X POST "https://api.meetstream.ai/api/v1/calendar/create_outlook_calendar" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "microsoft_client_id": "<YOUR_MICROSOFT_CLIENT_ID>",
    "microsoft_client_secret": "<YOUR_MICROSOFT_CLIENT_SECRET>",
    "microsoft_refresh_token": "<YOUR_MICROSOFT_REFRESH_TOKEN>"
  }'
```

### Response

```json
{
  "calendar_id": "outlook_calendar_usr_abc123",
  "platform": "outlook_calendar",
  "provider": "outlook",
  "account_id": "jane@acme.com",
  "user_email": "jane@acme.com",
  "user_name": "Jane Doe",
  "calendars": [
    {
      "id": "AQMkADAwATM0MDAA...",
      "summary": "Calendar",
      "isPrimary": true,
      "accessRole": "owner",
      "selected": true
    },
    {
      "id": "AQMkADAwATM0MDAB...",
      "summary": "Team Meetings",
      "isPrimary": false,
      "accessRole": "writer",
      "selected": true
    }
  ],
  "primary_calendar_id": "AQMkADAwATM0MDAA...",
  "watch_setup": {
    "success": true,
    "subscriptions_setup": 2,
    "subscription_results": [
      {
        "calendar_id": "AQMkADAwATM0MDAA...",
        "calendar_summary": "Calendar",
        "subscription_id": "a1b2c3d4-...",
        "expiration": "2026-05-23T22:00:00Z"
      }
    ],
    "failed_calendars": []
  },
  "message": "Calendar connected successfully"
}
```

### Connecting a second account

Run the OAuth helper again signed in as the second Microsoft account, get a new refresh token, then call `POST /create_outlook_calendar` again with that token. The new account is added to the MeetStream user; existing connections — Outlook or Google — are untouched. There's no separate "add account" endpoint, and there's no provider mode: the same endpoint handles both Microsoft and Google credentials based on which fields you send.

```bash
# Connect a second Outlook account (e.g. a work tenant) to the same MeetStream user.
curl -X POST "https://api.meetstream.ai/api/v1/calendar/create_outlook_calendar" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "microsoft_client_id": "<YOUR_MICROSOFT_CLIENT_ID>",
    "microsoft_client_secret": "<YOUR_MICROSOFT_CLIENT_SECRET>",
    "microsoft_refresh_token": "<REFRESH_TOKEN_FOR_THE_SECOND_ACCOUNT>"
  }'
```

### Reconnecting an existing account (token rotation)

If the same `account_id` is already connected for this MeetStream user, the call fails with **409 Conflict** by default:

```json
{
  "error": "Outlook account 'jane@acme.com' is already connected for this user. Pass {\"replace\": true} in the request body to reconnect."
}
```

Pass `"replace": true` in the body to rotate the refresh token / refresh the scopes for an already-connected account in place:

```json
{
  "microsoft_client_id": "...",
  "microsoft_client_secret": "...",
  "microsoft_refresh_token": "<NEW_REFRESH_TOKEN>",
  "replace": true
}
```

This is the right flow when your Azure client secret rotated or when you've extended the granted scopes — the credentials are updated without losing event history or notification subscriptions for that account.

### Limits

| Limit | Default |
|---|---|
| Calendar connections per MeetStream user (Google + Outlook combined) | **200** |

At the limit, `POST /create_outlook_calendar` returns `400` with `"Maximum of 200 outlook calendar connections per user reached…"`. Disconnect one before adding another (see §11), or contact support to raise the limit.

---

## 3) View your connections and calendars

You have two read endpoints: one for the **connections** themselves (which Microsoft accounts are attached, with metadata) and one for the **calendars within those connections** (live from Graph).

### 3a) List your calendar connections

```
GET https://api.meetstream.ai/api/v1/calendar/connections
```

Returns every connection on the authenticated MeetStream user, grouped by provider. No token material is ever returned.

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/connections" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

```json
{
  "user_id": "usr_abc123",
  "google": [],
  "outlook": [
    {
      "account_id": "jane@acme.com",
      "email": "jane@acme.com",
      "display_name": "Jane Doe (work)",
      "primary_calendar_id": "AQMkADAwATM0MDAA...",
      "scopes": ["Calendars.Read", "User.Read", "offline_access"],
      "created_at": "2026-04-10T11:20:00Z",
      "updated_at": "2026-04-10T11:20:00Z",
      "calendars_count": 12,
      "subscriptions_count": 12
    },
    {
      "account_id": "jane.personal@outlook.com",
      "email": "jane.personal@outlook.com",
      "display_name": "Jane Doe (personal)",
      "primary_calendar_id": "AQMkADAwATM0XYZ...",
      "scopes": ["Calendars.Read", "User.Read", "offline_access"],
      "created_at": "2026-04-12T09:05:00Z",
      "updated_at": "2026-04-12T09:05:00Z",
      "calendars_count": 4,
      "subscriptions_count": 4
    }
  ],
  "total": 2
}
```

#### Inspect one connection

```
GET https://api.meetstream.ai/api/v1/calendar/connections/{provider}/{account_id}
```

`{provider}` is `outlook` (the alias `microsoft` is accepted). `{account_id}` must be URL-encoded (`@` becomes `%40`, etc.).

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/connections/outlook/jane%40acme.com" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

Returns a single connection record like one of the entries above. **404** if the user has no connection for that account.

### 3b) List the calendars inside those connections

```
GET https://api.meetstream.ai/api/v1/calendar/calendars
```

Returns calendars across **every** connected account by default (Outlook + Google), with a per-connection summary so you can group them in your UI.

#### Query parameters

| Parameter | Required when… | Example |
|---|---|---|
| `provider` | You want to filter by provider only | `?provider=outlook` |
| `account_id` | You want only one specific account's calendars. **Requires `provider`**. | `?provider=outlook&account_id=jane%40acme.com` |

#### Example cURL

```bash
# All calendars from every connected account (Outlook + Google)
curl -X GET "https://api.meetstream.ai/api/v1/calendar/calendars" \
  -H "Authorization: Token <YOUR_API_KEY>"

# Only calendars from one specific Outlook account
curl -X GET "https://api.meetstream.ai/api/v1/calendar/calendars?provider=outlook&account_id=jane%40acme.com" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

#### Response

```json
{
  "calendars": [
    {
      "id": "AQMkADAwATM0MDAA...",
      "summary": "Calendar",
      "isPrimary": true,
      "accessRole": "owner",
      "timeZone": "America/New_York",
      "backgroundColor": "#0078d4",
      "selected": true,
      "provider": "outlook",
      "account_id": "jane@acme.com"
    },
    {
      "id": "AQMkADAwATM0XYZ...",
      "summary": "Personal",
      "isPrimary": true,
      "accessRole": "owner",
      "timeZone": "America/New_York",
      "backgroundColor": "#0078d4",
      "selected": true,
      "provider": "outlook",
      "account_id": "jane.personal@outlook.com"
    }
  ],
  "connections": [
    { "provider": "outlook", "account_id": "jane@acme.com", "calendars_count": 1, "is_legacy": false },
    { "provider": "outlook", "account_id": "jane.personal@outlook.com", "calendars_count": 1, "is_legacy": false }
  ],
  "total": 2,
  "user_id": "usr_abc123"
}
```

> This is a live call to the Microsoft Graph API and always returns the latest calendar list. Each entry carries `provider` and `account_id` so the dashboard / client can tell which connection it came from.
>
> If one connection fails to respond (revoked token, Graph throttling, expired client secret), the call still succeeds and surfaces the error under a `partial_errors` map keyed by `"{provider}::{account_id}"`.

---

## 4) Sync and view your events

MeetStream syncs events from every connected Outlook Calendar account and stores them locally. It detects meeting links for supported platforms (Google Meet, Zoom, Microsoft Teams, Webex, GoToMeeting, BlueJeans, and Whereby).

### Sync events from Outlook Calendar

```
GET https://api.meetstream.ai/api/v1/calendar/events
```

This is the primary events endpoint. It syncs with Outlook Calendar (using per-account Microsoft Graph delta tokens for speed), stores events in MeetStream's database, and returns them with pagination and linked bot information. By default, events from **every** connected account on the MeetStream user (Outlook + Google) are returned.

#### Query parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `calendar_id` | string | primary calendar of each connection | Specific calendar to sync |
| `time_min` | ISO 8601 | now - 1 day | Start of the time window |
| `time_max` | ISO 8601 | now + 28 days | End of the time window |
| `sync` | `"true"` / `"false"` | auto | Force a sync from Outlook Calendar |
| `limit` | int (1-100) | 50 | Number of events per page |
| `cursor` | string | — | Pagination cursor from `next` field |
| `cleanup_duplicates` | `"true"` / `"false"` | `"false"` | Deduplicate events |
| `provider` | `"outlook"` / `"google"` | all | Scope to one provider |
| `account_id` | string | all accounts | Scope to one specific connection. **Requires `provider`.** Use to fetch events from just one Outlook account when the user has several connected. |

#### Example cURL

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/events?limit=20" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

#### Response

```json
{
  "next": "eyJFdmVudElEIjogIm...",
  "previous": null,
  "results": [
    {
      "id": "evt_abc123",
      "start_time": "2026-04-08T15:00:00Z",
      "end_time": "2026-04-08T16:00:00Z",
      "calendar_id": "AQMkADAwATM0MDAA...",
      "platform": "outlook_calendar",
      "provider": "outlook",
      "account_id": "jane@acme.com",
      "platform_id": "outlook_evt_456",
      "ical_uid": "abc123@outlook.com",
      "meeting_platform": "Teams",
      "meeting_url": "https://teams.microsoft.com/l/meetup-join/...",
      "is_deleted": false,
      "created_at": "2026-04-01T12:00:00Z",
      "updated_at": "2026-04-07T09:30:00Z",
      "raw": { },
      "bots": [
        {
          "id": "bot_111",
          "status": "Scheduled",
          "scheduled_join_time": "2026-04-08T14:59:00+00:00",
          "bot_username": "MeetStream Calendar Bot",
          "platform": "Teams",
          "is_scheduled": true
        }
      ]
    }
  ],
  "has_more": false
}
```

Every event row carries a `provider` and `account_id` so you can group / filter client-side without re-issuing scoped requests. Use the `next` cursor value in a subsequent request to fetch the next page:

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/events?cursor=eyJFdmVudElEIjogIm..." \
  -H "Authorization: Token <YOUR_API_KEY>"
```

### Scoping to a single account

When the user has multiple connections, you can scope the sync + return to just one of them. This is faster (only one account is touched) and useful for per-tenant UIs.

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/events?provider=outlook&account_id=jane%40acme.com&limit=20" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

### Get events from local database only

```
GET https://api.meetstream.ai/api/v1/calendar/get_events
```

This is a lightweight endpoint that returns events already synced to MeetStream without calling the Microsoft Graph API. Useful when you want fast reads and don't need the latest sync. Accepts the same `provider` / `account_id` scoping query parameters as `/calendar/events`.

```bash
# All accounts (default)
curl -X GET "https://api.meetstream.ai/api/v1/calendar/get_events" \
  -H "Authorization: Token <YOUR_API_KEY>"

# One specific account
curl -X GET "https://api.meetstream.ai/api/v1/calendar/get_events?provider=outlook&account_id=jane%40acme.com" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

---

## 5) Schedule a bot for a specific meeting

Pick a meeting from your synced events and schedule a bot for it.

### API endpoint

```
POST https://api.meetstream.ai/api/v1/calendar/schedule/{event_id}
```

### Path parameters

| Parameter | Description |
|---|---|
| `event_id` | MeetStream event ID (the `id` field from the events list) |

### Request body

```json
{
  "bot_config": {
    "bot_name": "My Meeting Bot",
    "audio_required": true,
    "video_required": false,
    "bot_message": "Bot joining to record this meeting",
    "callback_url": "https://your-domain.com/webhooks/meetstream",
    "transcription": {
      "deepgram": { "model": "nova-3", "language": "en" }
    },
    "automatic_leave": {
      "no_one_joined_timeout": 300,
      "everyone_left_timeout": 60
    },
    "recording_config": {
      "video_recording": false
    }
  }
}
```

The `bot_config` accepts the same fields you'd normally pass to the Create Bot API.

### Example cURL

```bash
curl -X POST "https://api.meetstream.ai/api/v1/calendar/schedule/evt_abc123" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_config": {
      "video_required": false,
      "callback_url": "https://your-domain.com/webhooks/meetstream"
    }
  }'
```

### Response

```json
{
  "scheduled": true,
  "schedule_id": "bot-usr_abc1-bot_111aaa12",
  "bot_id": "bot_111...",
  "schedule_group": "teams",
  "event_id": "evt_abc123",
  "scheduled_time": "2026-04-08T14:59:00Z",
  "bot_config": { },
  "is_recurring_occurrence": false
}
```

### Scheduling options for recurring events

| Body field | Type | Default | Description |
|---|---|---|---|
| `occurrence_date` | ISO 8601 | — | Schedule a bot for a specific occurrence of a recurring event |
| `schedule_all_occurrences` | bool | `false` | Schedule bots for all future occurrences at once |
| `occurrence_limit` | int | 52 | Maximum number of occurrences to schedule when using `schedule_all_occurrences` |
| `recurring_event` | bool | `false` | Enable auto-rescheduling — after the bot joins this occurrence, automatically schedule the next one |

### Deduplication

If you schedule a bot for the same event twice, MeetStream returns a `409 Conflict` with the existing bot's ID. To update the bot config, use `PATCH /calendar/scheduled_bots/{bot_id}` instead.

---

## 6) Unschedule a bot

Cancel a scheduled bot for a specific event.

### API endpoint

```
DELETE https://api.meetstream.ai/api/v1/calendar/schedule/{event_id}
```

### Request body (optional)

```json
{
  "cancel_all_occurrences": false,
  "from_date": "2026-05-01T00:00:00Z"
}
```

| Body field | Type | Default | Description |
|---|---|---|---|
| `cancel_all_occurrences` | bool | `false` | Cancel bots for all occurrences of a recurring series |
| `from_date` | ISO 8601 | — | Only cancel occurrences from this date forward |

### Example cURL

```bash
curl -X DELETE "https://api.meetstream.ai/api/v1/calendar/schedule/evt_abc123" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

### Response

```json
{
  "unscheduled": true,
  "event_id": "evt_abc123",
  "cancelled_schedules": ["bot-usr_abc1-bot_111aaa12"],
  "schedules_cancelled": 1,
  "bots_deleted": 1,
  "cancel_all_occurrences": false,
  "is_recurring_series": false
}
```

---

## 7) Manage scheduled bots

View, update, or delete your scheduled bots across all events.

### List all scheduled bots

```
GET https://api.meetstream.ai/api/v1/calendar/scheduled_bots
```

Returns all bots with a scheduled join time in the future.

#### Query parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | int (1-100) | 100 | Maximum number of bots to return |

#### Example cURL

```bash
curl -X GET "https://api.meetstream.ai/api/v1/calendar/scheduled_bots" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

#### Response

```json
{
  "scheduled_bots": [
    {
      "bot_id": "bot_111...",
      "platform": "Teams",
      "status": "Scheduled",
      "scheduled_join_time": "2026-04-08T14:59:00+00:00",
      "bot_username": "MeetStream Calendar Bot",
      "meeting_link": "https://teams.microsoft.com/l/meetup-join/...",
      "custom_attributes": {
        "source": "calendar_integration",
        "event_id": "outlook_evt_456",
        "event_summary": "Weekly Standup"
      },
      "is_scheduled": true,
      "created_at": "2026-04-07T10:00:00Z"
    }
  ]
}
```

### Update a scheduled bot

```
PATCH https://api.meetstream.ai/api/v1/calendar/scheduled_bots/{bot_id}
```

Update the join time, display name, or other properties of a scheduled bot.

#### Request body

All fields are optional — include only what you want to change.

```json
{
  "scheduled_join_time": "2026-04-08T16:59:00Z",
  "bot_username": "Updated Bot Name",
  "custom_attributes": { "note": "VIP meeting" }
}
```

| Body field | Type | Description |
|---|---|---|
| `scheduled_join_time` | ISO 8601 | New join time (must be in the future). Updates the EventBridge schedule too. |
| `bot_username` | string | Display name for the bot in the meeting |
| `custom_attributes` | object | Custom metadata attached to the bot |

#### Response

```json
{
  "message": "Scheduled bot updated successfully",
  "bot_id": "bot_111...",
  "updated_fields": ["scheduled_join_time", "bot_username"],
  "schedule_updated": true
}
```

### Delete a specific scheduled bot

```
DELETE https://api.meetstream.ai/api/v1/calendar/scheduled_bots/{bot_id}
```

```bash
curl -X DELETE "https://api.meetstream.ai/api/v1/calendar/scheduled_bots/bot_111..." \
  -H "Authorization: Token <YOUR_API_KEY>"
```

```json
{
  "message": "Scheduled bot deleted successfully",
  "bot_id": "bot_111..."
}
```

---

## 8) Enable auto-scheduling

Auto-scheduling is a hands-free mode: MeetStream scans your calendar every 24 hours and automatically schedules bots for all upcoming meetings that have a meeting link.

### Enable auto-scheduling

```
POST https://api.meetstream.ai/api/v1/calendar/auto-schedule/enable
```

### Request body

Provide a default bot configuration that will be used for all auto-scheduled bots:

```json
{
  "default_bot_config": {
    "bot_name": "MeetStream Auto Bot",
    "audio_required": true,
    "video_required": false,
    "callback_url": "https://your-domain.com/webhooks/meetstream",
    "transcription": {
      "deepgram": { "model": "nova-3", "language": "en" }
    },
    "automatic_leave": {
      "no_one_joined_timeout": 300,
      "everyone_left_timeout": 60
    }
  }
}
```

### Example cURL

```bash
curl -X POST "https://api.meetstream.ai/api/v1/calendar/auto-schedule/enable" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "default_bot_config": {
      "video_required": false,
      "callback_url": "https://your-domain.com/webhooks/meetstream"
    }
  }'
```

### Response

```json
{
  "message": "Auto-scheduling enabled successfully",
  "auto_schedule_enabled": true,
  "default_bot_config": { }
}
```

### Disable auto-scheduling

```
POST https://api.meetstream.ai/api/v1/calendar/auto-schedule/disable
```

```bash
curl -X POST "https://api.meetstream.ai/api/v1/calendar/auto-schedule/disable" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

### Check auto-schedule settings

```
GET https://api.meetstream.ai/api/v1/calendar/auto-schedule/settings
```

```json
{
  "auto_schedule_enabled": true,
  "default_bot_config": {
    "bot_name": "MeetStream Auto Bot",
    "audio_required": true,
    "video_required": false
  }
}
```

### How auto-scheduling works

1. A background job runs **every 24 hours** at midnight UTC.
2. It finds all users who have auto-scheduling enabled.
3. For each user, it looks at upcoming events in the **next 24 hours** that have a valid meeting link.
4. It skips events that already have a bot scheduled (using deduplication keys).
5. It creates bot schedules using your `default_bot_config`.
6. Bots are scheduled to join **1 minute before** the meeting starts.

> You can override individual meetings by manually scheduling them with `POST /calendar/schedule/{event_id}` and a custom `bot_config`.

---

## 9) Recurring event auto-rescheduling

For recurring meetings (weekly standups, bi-weekly syncs, etc.), MeetStream can automatically schedule a bot for the **next occurrence** after each meeting ends.

### How it works

1. A bot joins a recurring meeting.
2. After the meeting ends, MeetStream detects it was a recurring event.
3. It calculates the next occurrence from the event's recurrence rule.
4. A new bot is automatically scheduled for the next occurrence using the same configuration.

This continues indefinitely — every recurring meeting gets a bot, without manual intervention.

### Enable auto-rescheduling when scheduling

When scheduling a bot for a recurring event, set `recurring_event: true` in the request body:

```bash
curl -X POST "https://api.meetstream.ai/api/v1/calendar/schedule/evt_abc123" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_config": { "video_required": false },
    "recurring_event": true
  }'
```

### Toggle auto-rescheduling for an existing event

You can enable or disable auto-rescheduling per event at any time:

```
POST https://api.meetstream.ai/api/v1/calendar/toggle-recurrence
```

```json
{
  "event_id": "evt_abc123",
  "recurring_enabled": true
}
```

#### Response

```json
{
  "event_id": "evt_abc123",
  "recurring_enabled": true,
  "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR",
  "message": "Recurrence enabled for event",
  "summary": "Weekly Team Standup",
  "start_time": "2026-04-07T10:00:00Z",
  "end_time": "2026-04-07T10:30:00Z"
}
```

> This endpoint returns 400 if the event is not actually a recurring event (no recurrence rule).

### Trigger rescheduling manually

MeetStream also exposes an endpoint to manually trigger rescheduling for the next occurrence:

```
POST https://api.meetstream.ai/api/v1/calendar/auto-reschedule
```

```json
{
  "user_id": "usr_abc123",
  "event_id": "evt_abc123",
  "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR",
  "current_start_time": "2026-04-07T10:00:00Z",
  "bot_config": { "video_required": false },
  "meeting_link": "https://teams.microsoft.com/l/meetup-join/..."
}
```

> This is primarily used internally by MeetStream after a meeting ends. You typically don't need to call it directly.

### Supported recurrence patterns

MeetStream supports standard iCalendar recurrence rules (RRULE) as surfaced by Microsoft Graph:

| Pattern | RRULE example |
|---|---|
| Daily | `FREQ=DAILY` |
| Weekly | `FREQ=WEEKLY;BYDAY=MO,WE,FR` |
| Bi-weekly | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TU` |
| Monthly | `FREQ=MONTHLY;BYDAY=1MO` (first Monday) |
| Yearly | `FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15` |

---

## 10) Real-time calendar change detection

When you connect your calendar, MeetStream sets up **Microsoft Graph change notification subscriptions** on your calendars. This enables real-time reactions to calendar changes without polling.

### What MeetStream handles automatically

| Calendar change | What MeetStream does |
|---|---|
| **Meeting rescheduled** (time changed) | Updates the bot's EventBridge schedule and `ScheduledJoinTime` to match the new time. The bot joins at the correct time. |
| **Meeting cancelled or deleted** | Cancels the EventBridge schedule and marks the bot record as `Cancelled`. No bot is created. |
| **Meeting time moved to the past** | Deletes the schedule and cancels the bot (since the meeting already happened or won't happen). |
| **New meeting added** | If auto-scheduling is enabled, a bot is scheduled for it during the next auto-schedule run. |

### How it works under the hood

1. Microsoft Graph detects a change and sends an HTTPS POST to a **per-account** webhook URL of the form `…/api/v1/admin/schedule/users/{user_id}/{provider}/{account_id}` — so a notification for `jane@acme.com` cannot be confused with a notification for `jane.personal@outlook.com` even when both are connected to the same MeetStream user.
2. MeetStream validates the notification using the per-subscription `clientState` secret issued at registration time and rejects mismatches.
3. MeetStream fetches updated event data from the Microsoft Graph API using the **per-account** delta token (one token per `account_id`, isolated from every other account's sync state).
4. For each event that has an active bot scheduled:
   - **Single events:** The existing EventBridge schedule is updated in place with the new time. The bot record's `ScheduledJoinTime` is also updated so `GET /scheduled_bots` reflects the correct time.
   - **Recurring events:** All existing schedules are deleted and recreated with the updated recurrence times.
   - **Cancelled events:** The schedule is deleted and the bot record is marked `Cancelled`.

Only events from the account that fired the notification are touched. Changes on other connections wait for their own notifications. You don't need to re-sync manually or make any API calls — changes are picked up in real time via webhooks from Microsoft.

### Subscription auto-renewal

Microsoft Graph change notification subscriptions for calendar events expire after approximately **3 days** (4,230 minutes). MeetStream automatically renews expiring subscriptions:

- A background job runs **daily at 3:00 AM UTC**.
- It checks all connected users' Graph subscriptions.
- Any subscription expiring within **2 days** is renewed automatically.
- No user action is required — webhooks stay active indefinitely.

> This is more frequent than Google Calendar's 30-day watch channels. MeetStream handles the renewal schedule transparently — you will not notice any gap in change detection.

---

## 11) Disconnect a calendar account

There are two disconnect endpoints. **For multi-account users, prefer the per-account variant** — it's explicit about which account is being removed and leaves every other connection alone.

### 11a) Disconnect one account (recommended)

```
DELETE https://api.meetstream.ai/api/v1/calendar/connections/{provider}/{account_id}
```

`{provider}` is `outlook` (the alias `microsoft` is accepted). `{account_id}` must be URL-encoded (`@` → `%40`).

#### What this does

1. Stops the Microsoft Graph change notification subscriptions registered for this connection (no more push notifications).
2. Deletes the per-account OAuth credentials from secure storage.
3. Removes the connection record from `GET /calendar/connections`.
4. **Cleans up events and scheduled bots that came from this account** — `Events` rows tagged with this `(provider, account_id)` are deleted; bots in `Scheduled` status for those events move to `Cancelled` with `reason: "calendar_disconnected"`. Bots that have already run are not touched.

Other Outlook (and Google) connections on this MeetStream user are completely unaffected.

#### Query parameter

| Parameter | Default | Description |
|---|---|---|
| `purge_events` | `true` | Set to `false` to disconnect the account without deleting its event history. The credentials and subscriptions are still removed (no more updates), but historical event rows stay in the database. |

#### Example cURL

```bash
# Disconnect the work account and purge its events + scheduled bots.
curl -X DELETE "https://api.meetstream.ai/api/v1/calendar/connections/outlook/jane%40acme.com" \
  -H "Authorization: Token <YOUR_API_KEY>"

# Disconnect but keep the event history for reporting.
curl -X DELETE "https://api.meetstream.ai/api/v1/calendar/connections/outlook/jane%40acme.com?purge_events=false" \
  -H "Authorization: Token <YOUR_API_KEY>"
```

#### Response

```json
{
  "found": true,
  "disconnected": true,
  "provider": "outlook",
  "account_id": "jane@acme.com",
  "purge_events": true,
  "webhook_cleanup": {
    "attempted": 12,
    "stopped": 12,
    "skipped_expired_or_invalid": 0
  },
  "events_cleanup": {
    "scope": { "provider": "outlook", "account_id": "jane@acme.com" },
    "events_matched": 47,
    "events_deleted": 47,
    "bots_cancelled": 6,
    "schedules_deleted": 6,
    "events_failed": 0
  },
  "message": "Outlook connection removed. Account events and scheduled bots cleaned up."
}
```

This endpoint is **idempotent** — re-calling it for an already-removed account returns `404 Not Found` without changing state.

### 11b) Disconnect (legacy single-account endpoint)

```
DELETE https://api.meetstream.ai/api/v1/calendar/disconnect
```

Original endpoint, kept for backward compatibility. Behaviour depends on how many connections the MeetStream user has:

| Scenario | Body | Behaviour |
|---|---|---|
| User has **one** connection | `{}` | Tears down that one connection. |
| User has **multiple** connections | `{}` | **400 Bad Request** — refuses to disconnect everything implicitly. Use 11a per account or pass an explicit `account_id`. |
| Explicit per-account disconnect | `{"provider": "outlook", "account_id": "jane@acme.com"}` | Same effect as the §11a endpoint. |
| Provider-scoped disconnect | `{"provider": "outlook"}` | Removes **every** Outlook connection on the user (and only Outlook). Useful when migrating tenants. |
| Same as above, keep events | `{"provider": "outlook", "account_id": "jane@acme.com", "purge_events": false}` | Or `?purge_events=false` in the query string. |

The 400 response includes a `next_steps` hint so callers know exactly what to do:

```json
{
  "error": "Multiple calendar connections present; refusing to disconnect all without explicit account_id",
  "scope": "all",
  "connection_count": 3,
  "next_steps": "Call DELETE /api/v1/calendar/connections/{provider}/{account_id} per account, or POST this endpoint with body {\"provider\": \"...\", \"account_id\": \"...\"}."
}
```

This safety check exists so that one careless API call cannot accidentally wipe out a multi-tenant user's entire calendar history.

> Both disconnect paths are irreversible. To reconnect an account, call `POST /calendar/create_outlook_calendar` again with its credentials.

---

## End-to-end setup checklist

Here's the full flow to get Outlook Calendar integration running:

1. **Get credentials** — Register an app in Azure Portal, add Microsoft Graph permissions, and run the OAuth helper to get a refresh token for each Microsoft account you want to connect
2. **Connect** — `POST /calendar/create_outlook_calendar` once per account. Each call attaches that account to the same MeetStream user.
3. **Verify** — `GET /calendar/connections` to confirm every account you wanted is attached
4. **Sync** — `GET /calendar/events` to pull in meetings from every connected account (or pass `?provider=outlook&account_id=…` to scope)
5. **Schedule** — `POST /calendar/schedule/{event_id}` to add a bot to a specific meeting
6. **Or auto-schedule** — `POST /calendar/auto-schedule/enable` to cover all meetings across all connected accounts automatically
7. **Recurring** — Set `recurring_event: true` when scheduling, or use `POST /calendar/toggle-recurrence`
8. **Relax** — MeetStream handles calendar changes, rescheduling, cancellations, and subscription renewal per-account from here

---

## API quick reference

| Method | Endpoint | Description |
|---|---|---|
| **Calendar connections** | | |
| POST | `/api/v1/calendar/create_outlook_calendar` | Connect one Outlook (or Google) account. Call repeatedly for additional accounts. Pass `{"replace": true}` to rotate tokens on an existing account. |
| GET | `/api/v1/calendar/connections` | List every connected account on the user, grouped by provider |
| GET | `/api/v1/calendar/connections/{provider}/{account_id}` | Inspect one connection's metadata |
| DELETE | `/api/v1/calendar/connections/{provider}/{account_id}` | Disconnect one account (preferred). Optional `?purge_events=false` keeps event history. |
| DELETE | `/api/v1/calendar/disconnect` | Legacy disconnect. Returns 400 when the user has >1 connection unless body includes `{provider, account_id}`. |
| GET | `/api/v1/calendar/calendars` | List calendars across every connected account (live from Microsoft Graph). `?provider=&account_id=` to scope to one. |
| **Events** | | |
| GET | `/api/v1/calendar/events` | Sync and list events. Default = every account. `?provider=&account_id=` to scope. |
| GET | `/api/v1/calendar/get_events` | List events from local database only (fast). Same `?provider=&account_id=` scoping. |
| **Bot scheduling** | | |
| POST | `/api/v1/calendar/schedule/{event_id}` | Schedule a bot for an event |
| DELETE | `/api/v1/calendar/schedule/{event_id}` | Unschedule a bot for an event |
| **Scheduled bot management** | | |
| GET | `/api/v1/calendar/scheduled_bots` | List all upcoming scheduled bots |
| PATCH | `/api/v1/calendar/scheduled_bots/{bot_id}` | Update a scheduled bot (time, name, attributes) |
| DELETE | `/api/v1/calendar/scheduled_bots/{bot_id}` | Delete a specific scheduled bot |
| **Auto-scheduling** | | |
| POST | `/api/v1/calendar/auto-schedule/enable` | Enable auto-scheduling with default bot config |
| POST | `/api/v1/calendar/auto-schedule/disable` | Disable auto-scheduling |
| GET | `/api/v1/calendar/auto-schedule/settings` | Get current auto-schedule settings |
| **Recurring events** | | |
| POST | `/api/v1/calendar/auto-reschedule` | Trigger rescheduling for next recurring occurrence |
| POST | `/api/v1/calendar/toggle-recurrence` | Enable/disable auto-rescheduling per event |

---

## FAQ

### Which calendar providers are supported?
**Google Calendar** and **Outlook Calendar** (Microsoft 365 / Outlook.com) are both supported on the same MeetStream user. Your calendar can contain meetings from any platform — MeetStream detects Google Meet, Zoom, Microsoft Teams, Webex, GoToMeeting, BlueJeans, and Whereby links. See the [Google Calendar Integration Guide](/guides/app-integrations/using-credentials-with-meetstream-api) for the Google flow.

### Do I need a Microsoft 365 (work/school) account?
No. Both personal Microsoft accounts (Outlook.com, Hotmail, Live) and Microsoft 365 work/school accounts are supported. When registering your app in Azure Portal, choose **Accounts in any organizational directory and personal Microsoft accounts** to cover both. You can mix and match: one MeetStream user can connect a personal Outlook.com account and multiple work tenants at the same time.

### Can one MeetStream user connect multiple Microsoft accounts?
Yes — up to **200** calendar connections per MeetStream user (Outlook + Google combined). Call `POST /create_outlook_calendar` once per refresh token. Each connection is identified by `(provider="outlook", account_id=<the account's primary email>)`. Use `GET /calendar/connections` to inspect them, and `DELETE /calendar/connections/{provider}/{account_id}` to remove one without affecting the others.

### What happens if I call `POST /create_outlook_calendar` for the same Microsoft account twice?
The second call returns **409 Conflict** with the existing `account_id`. To rotate the refresh token or refresh the scopes for an already-connected account, pass `"replace": true` in the request body — the credentials are updated in place without losing event history or notification subscriptions.

### Can I scope `GET /calendar/events` to just one of my connected accounts?
Yes. Pass `?provider=outlook&account_id=<URL-encoded-email>`. Without those query parameters, events from every connected account are returned (each row carries `provider` and `account_id` so you can group them client-side).

### How does MeetStream avoid mixing up webhook notifications between my connected accounts?
Each connection has its own webhook URL of the form `…/api/v1/admin/schedule/users/{user_id}/{provider}/{account_id}`, and an opaque per-subscription `clientState` issued at registration. Notifications that arrive on the wrong URL or carry a mismatched `clientState` are rejected. Delta tokens, calendar lists, and notification subscriptions are all stored per-account in isolated storage paths.

### I only have one Outlook account connected — do I need to worry about any of this multi-account stuff?
No. Every endpoint behaves the same way when you have exactly one connection — you can keep ignoring `provider`, `account_id`, and `/connections`. They become useful the moment you connect a second account.

### How do I get a Microsoft OAuth2 refresh token?
Follow **Section 1** of this guide — register an app in Azure Portal, add the required Microsoft Graph permissions, and run the provided OAuth helper (`node server_microsoft.js`) to complete the consent flow. The helper displays your refresh token in the browser.

### How far in advance does auto-scheduling look?
The auto-schedule job runs every 24 hours at midnight UTC and schedules bots for meetings happening in the **next 24 hours**. Meetings further out will be picked up in subsequent runs.

### When does the bot join relative to the meeting start time?
Bots are scheduled to join **1 minute before** the meeting's start time.

### What happens if I reschedule a meeting in Outlook?
MeetStream receives a real-time push notification from Microsoft Graph and automatically updates the bot's scheduled join time to match the new meeting time. Both the EventBridge schedule and the bot record in MeetStream's database are updated, so `GET /scheduled_bots` always shows the correct time.

### What if I cancel a meeting?
MeetStream detects the cancellation via the Microsoft Graph notification, deletes the EventBridge schedule, and marks the bot as `Cancelled`. No bot will be created for a cancelled meeting.

### What if a meeting is moved to a time that already passed?
MeetStream detects that the new time is in the past, deletes the EventBridge schedule, and cancels the bot record. This prevents a bot from being created for a meeting that can no longer be joined.

### Can I schedule bots for meetings without a meeting link?
No. MeetStream requires a valid meeting link (Google Meet, Zoom, Teams, etc.) to join. Events without a detected meeting link are skipped.

### What happens if I schedule a bot for the same event twice?
MeetStream deduplicates by event. The second call returns a `409 Conflict` with the existing bot's ID. To update the bot config, use `PATCH /calendar/scheduled_bots/{bot_id}`.

### Can I use different bot configurations for different meetings?
Yes. When you manually schedule a bot via `POST /calendar/schedule/{event_id}`, you provide the `bot_config` per event. Auto-scheduling uses your `default_bot_config` for all meetings, but you can override individual events by scheduling them manually.

### Does auto-rescheduling work with all recurrence patterns?
MeetStream supports standard iCalendar recurrence rules (RRULE) — daily, weekly, bi-weekly, monthly, yearly, and custom patterns. The next occurrence is calculated from the event's recurrence rule as returned by Microsoft Graph.

### How do I stop a recurring event from being rescheduled?
Use the toggle endpoint: `POST /calendar/toggle-recurrence` with `"recurring_enabled": false` for that event.

### Do I need to worry about Microsoft Graph subscriptions expiring?
No. MeetStream automatically renews Microsoft Graph change notification subscriptions before they expire. A daily background job checks for subscriptions expiring within 2 days and renews them. Your real-time calendar sync stays active indefinitely without any action on your part.

### What data is deleted when I disconnect my calendar?
- **Per-account disconnect (`DELETE /calendar/connections/{provider}/{account_id}`):** Graph subscriptions for that account are stopped, its credentials are wiped from secure storage, the connection record is removed, and (by default) events from that account along with their scheduled bots are deleted. Pass `?purge_events=false` to keep the event history. Other connected accounts are completely untouched.
- **Full-user disconnect (`DELETE /calendar/disconnect`):** the legacy endpoint. When only one connection exists, it tears that one down. When multiple connections exist, it refuses (returns 400) unless you specify which account in the body — to prevent accidentally wiping out a multi-tenant user's entire history.

Both paths are irreversible. To reconnect, call `POST /calendar/create_outlook_calendar` again.

### Is my Outlook Calendar data stored securely?
Yes. OAuth credentials are stored in AWS Systems Manager Parameter Store as encrypted `SecureString` parameters. Event data is stored in DynamoDB with encryption at rest. MeetStream does not store your Microsoft password.

### Can I schedule bots for all occurrences of a recurring event at once?
Yes. Pass `"schedule_all_occurrences": true` in the request body when scheduling. You can limit the number of occurrences with `"occurrence_limit"` (default 52). Each occurrence gets its own bot and EventBridge schedule.

### How do I schedule a bot for a specific occurrence of a recurring event?
Pass `"occurrence_date": "2026-04-14T10:00:00Z"` in the request body. MeetStream will schedule a bot for that specific occurrence only.

### Can I cancel bots for future occurrences only?
Yes. When unscheduling a recurring event, pass `"from_date": "2026-05-01T00:00:00Z"` to cancel only occurrences from that date forward. Occurrences before that date keep their scheduled bots.

### My client secret expired — what do I do?
Generate a new client secret in Azure Portal under your app's **Certificates & secrets**, then re-run the OAuth helper to get a fresh refresh token, and call `POST /calendar/create_outlook_calendar` again with the new credentials. MeetStream will replace the old connection automatically.

---

For webhook event handling, see the [Webhook Events Guide](MeetStream_Bot_Lifecycle_Webhook_Events_Guide.md).
For creating your first bot without calendar integration, see the [First Bot Quickstart](/guides/get-started/create-your-first-bot).
For Google Calendar integration, see the [Google Calendar Integration Guide](/guides/app-integrations/using-credentials-with-meetstream-api).