> 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 — Zoom Authenticated Bots (ZAK and OBF)

How to send a MeetStream Zoom bot as a **signed-in user (ZAK)** or **on behalf of a user already in the meeting (OBF)**.

MeetStream does **not** store your end-users’ Zoom OAuth refresh tokens for join. You run Zoom OAuth, keep the refresh token, and expose an HTTPS URL. At join, the bot **fetches that URL** and uses the token you return.

**Endpoint:** `POST /api/v1/bots/create_bot`  
**Auth:** `Authorization: Token <your MeetStream user API key>`

---

## 1) Pick a join mode

| `zoom` on create_bot | What happens |
| --- | --- |
| omitted or `{}` | **Guest** join — no ZAK, no OBF |
| `{ "zak_url": "https://..." }` | **Signed-in** — bot joins as the Zoom user whose token you mint |
| `{ "obf_url": "https://..." }` | **On-behalf-of** — bot is tied to a parent user **already in that meeting** |
| both URLs | `400` — send only one |

Do **not** send `use_zoom_obf` or `zoom_oauth_connection_user_id`. Those fields are rejected.

You still save your Zoom **Marketplace app** `client_id` / `client_secret` on the MeetStream user (integrations `auth` / `zoom`). That is the Meeting SDK app the **bot process** runs as. It is not the end-user OAuth grant.

URLs must be `https` with a host (max 4096 characters). Bots run in AWS and cannot reach `localhost`.

---

## 2) ZAK vs OBF (Zoom behavior)

| | ZAK (`zak_url`) | OBF (`obf_url`) |
| --- | --- | --- |
| Zoom token | `GET /users/me/token?type=zak` | `GET /users/me/token?type=onbehalf&meeting_id={id}` |
| Typical OAuth scope | `user:read:zak` | `user:read:token` (add `user:read:user` if you key by Zoom user id) |
| Who the bot is | That Zoom user (signed-in, profile photo) | Assistant associated with a parent in the call |
| Parent must already be in the meeting | No — subject to Zoom host privileges and meeting admission settings | **Yes** |
| If that Zoom user leaves | Bot can stay | Zoom **removes** the bot |
| Meeting id on your URL | Not required | **Do not** put `meeting_number` on the URL. MeetStream appends it at join |

Enable **Meeting SDK** on the Zoom General App when using OBF.

OBF tokens are short-lived and single-use. Mint them **when MeetStream calls you**, not at create-bot time.

---

## 3) What you host

Two jobs, both on **your** HTTPS server:

1. **OAuth (once per Zoom user or service account)**  
   Redirect to Zoom authorize → callback with `code` → `POST https://zoom.us/oauth/token` (`grant_type=authorization_code`) → store `refresh_token`. Never send refresh tokens to MeetStream.

2. **Mint endpoint (every join)**  
   When the bot calls your URL, refresh the access token if needed (`grant_type=refresh_token`; persist a new refresh token if Zoom returns one), call Zoom’s user-token API, return the ZAK or OBF as **plain text**.

Encode **which user** in the URL (`user_id`, signed token, etc.). Treat the URL as a credential (shared `auth` query, HMAC, or short-lived signed URL).

---

## 4) create_bot examples

### Guest (no authenticated Zoom join)

Omit `zoom`, or send `"zoom": {}`.

### Signed-in (ZAK)

```json
{
  "meeting_link": "https://zoom.us/j/123456789?pwd=...",
  "bot_name": "MeetStream Bot",
  "audio_required": true,
  "zoom": {
    "zak_url": "https://api.yourapp.com/zoom/zak?user_id=alice&auth=YOUR_SECRET"
  }
}
```

### On-behalf-of (OBF)

Join the Zoom meeting **yourself first**, then create the bot. Do **not** add `meeting_number` to `obf_url`.

```json
{
  "meeting_link": "https://zoom.us/j/123456789?pwd=...",
  "bot_name": "MeetStream Bot",
  "audio_required": true,
  "automatic_leave": {
    "waiting_room_timeout": 1200
  },
  "zoom": {
    "obf_url": "https://api.yourapp.com/zoom/obf?user_id=alice&auth=YOUR_SECRET"
  }
}
```

The parent must already be in the meeting before the OBF join attempt. A longer waiting-room timeout does not replace that requirement.

---

## 5) What MeetStream sends to your URL

The Zoom bot tries **GET first**. If the server returns **405**, or the body cannot be parsed as a token, it **POSTs JSON**. A **400** on GET is **not** retried as POST — fix the handler.

Common to both:

| | |
| --- | --- |
| Header | `X-Bot-Id`: MeetStream bot id |
| Header | `X-Webhook-Secret` if the bot has a webhook secret |
| Success | `200`, `Content-Type: text/plain`, raw token (length &gt; 50), **or** JSON `{"token": "TOKEN"}` / `{"zak": "TOKEN"}` / `{"obf": "TOKEN"}` |
| Failure | Non-2xx. If a URL is set, the bot **does not** guest-join |

**ZAK:** POST body `{ "bot_id", "webhook_secret"? }`. No `meeting_number`.

**OBF:** GET query `meeting_number` (MeetStream adds this). POST body `{ "bot_id", "meeting_number", "webhook_secret"? }`. Mint with that meeting id.

If you already put `meeting_number` on `obf_url`, MeetStream appends it again. Many frameworks then see an **array** and your handler returns 400 (`OBF token is required but could not be fetched`). Leave meeting id off the create-bot URL.

---

## 6) Test without a bot

After OAuth, curl your own host (replace host, secret, meeting id):

```bash
# ZAK
curl -sS -D - -H "X-Bot-Id: test-bot" \
  "https://YOUR_HOST/zoom/zak?user_id=alice&auth=YOUR_SECRET"

# OBF
curl -sS -D - -H "X-Bot-Id: test-bot" \
  "https://YOUR_HOST/zoom/obf?user_id=alice&auth=YOUR_SECRET&meeting_number=123456789"
```

Expect `200` and a long token. `401` without `auth` is expected if you use a shared secret.

Then create_bot with `zak_url` or `obf_url` pointing at the **same** URL **without** duplicating `meeting_number` on OBF.

---

## 7) Troubleshooting

| Symptom | What to check |
| --- | --- |
| `Pass only one of zoom.zak_url or zoom.obf_url` | Send exactly one URL. |
| `must use https` / `must include a host` | Public HTTPS URL. |
| `use_zoom_obf is no longer supported` | Use `zak_url` or `obf_url` and host OAuth yourself. |
| `Custom zoom.zak_url/zoom.obf_url are not supported` | API build is still on the old hosted-OBF contract. |
| Create succeeds, join: `OBF token is required but could not be fetched` | Your URL returned non-2xx. Check duplicate `meeting_number`, ngrok down, `401` auth, Zoom mint error. |
| OBF never admitted / waiting room forever | Parent user must be **in the meeting**. Increase `waiting_room_timeout`. |
| Guest bot when you expected ZAK/OBF | URL omitted or not forwarded; check create-bot body. |

---

## 8) Related

- [Zoom Marketplace App Setup](/guides/app-integrations/zoom-marketplace-app-setup)
- [Create Bot Payload Reference](/api-reference/create-bot-payload-reference)
- [Create Bot API](/api-reference/api-endpoints/bot-endpoints/create-bot)