Zoom OBF implementation in MeetStream

View as Markdown

This guide walks you through connecting your end-users’ Zoom accounts to MeetStream so bots can join their Zoom meetings using MeetStream’s hosted OBF (OAuth-based) flow. Each end-user authorizes the same Zoom Marketplace app you registered, and MeetStream stores one connection per end-user. At create-bot time you tell MeetStream which end-user the bot should act on behalf of by passing that end-user’s Zoom user id.

Production API base URL: https://api.meetstream.ai (use your environment’s host if different).


What you need

  • A Zoom account with permission to create apps in the Zoom App Marketplace.
  • The MeetStream User Dashboard to manage Zoom OAuth connections (the dashboard obtains the Zoom authorize URL and sends the user’s browser there).
  • A deployed HTTPS server you operate at your redirect URL (for example https://yourapp.example.com/zoom/oauth/callback). After Zoom redirects an end-user’s browser to this URL with an authorization code, this server must call MeetStream’s connection-create endpoint (see Step 2). MeetStream does not complete OAuth in the browser at your callback — your backend must call the connection-create endpoint. A minimal Node example lives in this repo at examples/zoom-meetstream-oauth-callback.
  • The same MeetStream user API key (Authorization: Token …) for authorize-url (used when starting each end-user’s flow) and connection-create (used on your server). The same redirect URI string everywhere: Zoom app, authorize-url, and connection-create.

Step 1: Create a Zoom app and set the redirect URL

  1. Go to the Zoom App Marketplace and sign in.

  2. Create a single OAuth app for your product. You only register ONE app, even if you have many end-users. Each end-user authorizes that same app under their own Zoom account.

  3. In your Zoom app’s OAuth settings, add your redirect URL (for example https://yourapp.example.com/zoom/oauth/callback). The same string must appear in:

    • Zoom’s OAuth Redirect URL / allow list fields (per Zoom’s UI), and
    • The redirect_uri query parameter when MeetStream’s authorize-url API is called, and
    • The JSON redirect_uri field on POST https://api.meetstream.ai/api/v1/zoom/oauth/connections.

    All three must match byte-for-byte (scheme https, host, path, no unintended trailing slash).

  4. Required scopes: user:read:user, user:read:token (so MeetStream can identify each authorizing end-user and mint OBF tokens on their behalf).

  5. App distribution: depends on your scenario.

    • If your end-users are inside a single Zoom account (e.g. an internal tool), an account-level / internal app is fine.
    • If your end-users are spread across many Zoom accounts (typical B2B SaaS), publish your app on the Zoom Marketplace so it can be installed by any Zoom account.

For development, use the same browser session for editing the Zoom app and for completing consent if Zoom or your tunnel (e.g. ngrok) relies on cookies.


Step 2: One OAuth flow per end-user (creates a Zoom OAuth Connection)

You will run this flow once per end-user. Each completed flow stores a separate connection on MeetStream, keyed by the end-user’s Zoom user id.

  1. In your product’s UI, when an end-user (e.g. Alice) clicks Connect Zoom:

    • Your backend calls GET https://api.meetstream.ai/api/v1/zoom/oauth/authorize-url?redirect_uri=<https...>&state=<your-end-user-id> with Authorization: Token <user_api_key>.
    • The optional state query parameter is round-tripped to Zoom and back to your callback URL — use it to carry your own end-user identifier (e.g. alice-internal-uuid) through the OAuth flow.
    • Open the returned authorize_url in the end-user’s browser.
  2. The end-user signs in to their own Zoom account and approves the app. Zoom redirects their browser to your redirect URI with ?code=<auth_code>&state=<your-end-user-id>.

  3. Your server receives the request and calls MeetStream:

    POST https://api.meetstream.ai/api/v1/zoom/oauth/connections

    • Header: Authorization: Token <user_api_key>
    • Body (JSON):
      1{
      2 "code": "<from Zoom redirect>",
      3 "redirect_uri": "<exact same redirect URL as Step 1>",
      4 "metadata": { "tenant_user_id": "<your end-user id>" }
      5}
    • Response (200):
      1{
      2 "zoom_user_id": "<id from Zoom /users/me>",
      3 "zoom_account_id": "<zoom account id>",
      4 "email": "<zoom email>",
      5 "display_name": "<zoom display name>",
      6 "metadata": { "tenant_user_id": "<your end-user id>" },
      7 "state": "connected",
      8 "created_at": "...",
      9 "updated_at": "...",
      10 "has_refresh_token": true
      11}

    Save zoom_user_id against the end-user record in your own DB. You will pass it back when creating bots for this end-user.

  4. Repeat steps 1–3 for every additional end-user (Bob, Carol, …). Each flow uses the same Zoom Marketplace app (same client_id / client_secret); only the authorizing end-user changes.

If anything fails, confirm the three redirect_uri strings match Zoom’s registered URL and that the same user API key is used for authorize-url and connection-create.

Listing, fetching, and deleting connections

MethodPathPurpose
GET/api/v1/zoom/oauth/connectionsList all Zoom OAuth connections for your account
GET/api/v1/zoom/oauth/connections/{zoom_user_id}Get a single connection (no token material)
DELETE/api/v1/zoom/oauth/connections/{zoom_user_id}Disconnect that Zoom user

All endpoints use Authorization: Token <user_api_key>. Token material (access_token / refresh_token) is never returned by these endpoints; only identity, metadata, state, and timestamps.


Step 3: Create a bot that joins as a specific end-user (create_bot)

When you create a bot for Alice’s meeting, pass Alice’s Zoom user id (the one returned by the connections endpoint when Alice authorized) under zoom:

1{
2 "meeting_link": "https://zoom.us/j/123456789?pwd=...",
3 "bot_name": "MeetStream Bot",
4 "audio_required": true,
5 "video_required": false,
6 "zoom": {
7 "use_zoom_obf": true,
8 "zoom_oauth_connection_user_id": "<Alice's zoom_user_id>"
9 }
10}
  • meeting_link must be a Zoom meeting URL.
  • use_zoom_obf must be a boolean (true or false).
  • zoom_oauth_connection_user_id is required when use_zoom_obf=true. It must match the zoom_user_id returned by POST /api/v1/zoom/oauth/connections for the end-user this bot should act on behalf of.

Do not send custom token server fields such as zoom.obf_url or zoom.zak_url; MeetStream supplies those when hosted Zoom auth is enabled.


Troubleshooting

IssueWhat to try
zoom.zoom_oauth_connection_user_id is required when zoom.use_zoom_obf=trueAdd the field to your create_bot payload using the value returned from POST /api/v1/zoom/oauth/connections.
No Zoom users connected for this accountThe end-user has not completed the OAuth flow. Send them through GET /api/v1/zoom/oauth/authorize-url and complete the connection.
Zoom user <id> is not connected for this accountThe zoom_user_id you passed does not match any stored connection under your MeetStream account. Verify the value and that you’re using the right MeetStream user API key.
Zoom user <id> is disconnected (no valid refresh token)Zoom revoked the refresh token (password change, app uninstalled, idle 90+ days). Have that end-user reconnect via the OAuth flow.
Redirect or “invalid redirect” from ZoomThe URL Zoom uses must exactly match the Zoom app, authorize-url redirect_uri, and connection-create redirect_uri.
Error that use_zoom_obf is invalidEnsure it is a JSON boolean, not a string.
Bot created but meeting is not Zoomuse_zoom_obf is only for Zoom links; use a Zoom meeting_link.

Summary

  1. Zoom Marketplace: Register one HTTPS callback URL (e.g. …/zoom/oauth/callback) for your single Zoom OAuth app.
  2. Per end-user OAuth: authorize-url (with state=<your end-user id>) → end-user authorizes → your callback calls POST /api/v1/zoom/oauth/connections → save the returned zoom_user_id against your end-user record.
  3. create_bot: Add "zoom": { "use_zoom_obf": true, "zoom_oauth_connection_user_id": "<that end-user's zoom_user_id>" } for Zoom meetings.