# Zoom

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Zoom by building a Firebase Cloud Function that performs the Server-to-Server OAuth token exchange and creates Zoom meetings, then calling that function from a FlutterFlow API Call. Zoom's Account ID, Client ID, and Client Secret cannot live in a compiled Flutter app — they must stay server-side. The function returns the meeting's join_url for display in your FlutterFlow app.

## Create Zoom Meetings from Your FlutterFlow App Using Server-to-Server OAuth

Adding Zoom meeting creation to a FlutterFlow app is a compelling feature — a user taps a button, a meeting is instantly created, and the join link appears ready to share. Zoom's REST API makes this possible, but the auth model requires a Cloud Function. Zoom deprecated its legacy JWT authentication in September 2023; the current standard is Server-to-Server OAuth: Account ID + Client ID + Client Secret are used to request a 1-hour Bearer token, and that token authenticates all Zoom API calls.

Those credentials cannot live in a FlutterFlow app. FlutterFlow compiles to Flutter code running on the user's device — any key embedded in an API Call header or in Dart code ships in the app binary and can be extracted. A Firebase Cloud Function holds all Zoom credentials, mints the 1-hour token (and caches it to avoid re-minting on every call), creates the meeting, and returns the join URL. FlutterFlow calls that function URL, never Zoom's endpoints directly.

Zoom's free account is sufficient to register a Server-to-Server OAuth app in the Zoom Marketplace. Meeting creation, listing, and recording retrieval are all available on free accounts. Participant reports (`/report/meetings/{id}/participants`) require the `report:read:admin` scope and are available 30–60 minutes after a meeting ends — not in real-time. This page focuses on the most common use case: creating a meeting and returning the join URL, with a section on recordings for dashboard features.

## Before you start

- A Zoom account (free tier is sufficient) with access to the Zoom Marketplace at marketplace.zoom.us
- A Firebase project with Cloud Functions enabled on the Blaze pay-as-you-go plan (required for external network calls)
- A FlutterFlow project connected to Firebase (or Supabase) for storing meeting data
- Node.js installed locally for Cloud Function development
- Basic understanding of Firebase Functions config and deployment

## Step-by-step guide

### 1. Create a Server-to-Server OAuth App in Zoom Marketplace

Open your browser and go to marketplace.zoom.us. Click 'Develop' in the top navigation and select 'Build App'. Choose 'Server-to-Server OAuth' as the app type — this replaces the deprecated JWT app type and is the correct choice for server-side API access where there is no user login to Zoom required. Give your app a name (e.g., 'FlutterFlow Integration'), fill in the short and long description, and click Continue. On the Scopes page, add the scopes your Cloud Function needs: `meeting:write:admin` to create meetings on behalf of account users, `meeting:read:admin` to list and retrieve meetings, and `recording:read:admin` if you want to pull cloud recording links. If you plan to access participant reports, add `report:read:admin`. Click Continue to complete the app registration. On the App Credentials page, copy three values: **Account ID**, **Client ID**, and **Client Secret**. These are your Zoom credentials. Keep them in a secure location — you will add them to Firebase Functions environment config in the next step, and they must never be copied into FlutterFlow.

**Expected result:** A Server-to-Server OAuth app is created in the Zoom Marketplace. Account ID, Client ID, and Client Secret are copied and ready to add to Firebase Functions config.

### 2. Write and Deploy a Firebase Cloud Function that Mints Zoom Tokens and Creates Meetings

This Cloud Function is the heart of the integration. It holds the Zoom credentials securely, exchanges them for a 1-hour Bearer token, and creates (or lists) Zoom meetings. To avoid re-minting the token on every request, cache the token and its expiry time in memory (function-level variable) or in Firestore — and invalidate it when it nears the 3600-second TTL. Set your Zoom credentials in Firebase Functions config by running in your terminal: `firebase functions:config:set zoom.account_id="your-account-id" zoom.client_id="your-client-id" zoom.client_secret="your-client-secret"`. Then deploy the function below with `firebase deploy --only functions`. The function exposes an HTTP endpoint that FlutterFlow calls. It accepts the meeting parameters in the JSON body (topic, start time, duration, timezone) and returns the created meeting's `join_url` and `id`. For a simpler architecture, the function does everything: get token + create meeting + return URL — one call from FlutterFlow, one result.

```
// functions/index.js — Zoom meeting creator
const functions = require('firebase-functions');
const fetch = require('node-fetch');

const ZOOM_ACCOUNT_ID = functions.config().zoom.account_id;
const ZOOM_CLIENT_ID = functions.config().zoom.client_id;
const ZOOM_CLIENT_SECRET = functions.config().zoom.client_secret;
const ZOOM_TOKEN_URL = 'https://zoom.us/oauth/token';
const ZOOM_API_BASE = 'https://api.zoom.us/v2';

// In-memory token cache (valid for ~55 min)
let cachedToken = null;
let tokenExpiresAt = 0;

async function getZoomToken() {
  const now = Date.now();
  // Refresh if cached token expires within 5 minutes
  if (cachedToken && now < tokenExpiresAt - 300000) {
    return cachedToken;
  }
  const credentials = Buffer.from(`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`).toString('base64');
  const res = await fetch(
    `${ZOOM_TOKEN_URL}?grant_type=account_credentials&account_id=${ZOOM_ACCOUNT_ID}`,
    {
      method: 'POST',
      headers: {
        Authorization: `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );
  const data = await res.json();
  if (!data.access_token) throw new Error('Failed to get Zoom token: ' + JSON.stringify(data));
  cachedToken = data.access_token;
  tokenExpiresAt = now + data.expires_in * 1000;
  return cachedToken;
}

exports.createZoomMeeting = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
  if (req.method !== 'POST') { res.status(405).json({ error: 'POST only' }); return; }

  const {
    topic = 'Meeting',
    type = 2, // 2 = scheduled, 1 = instant
    start_time,
    duration = 60,
    timezone = 'UTC',
    userId = 'me',
  } = req.body;

  try {
    const token = await getZoomToken();
    const zoomRes = await fetch(`${ZOOM_API_BASE}/users/${userId}/meetings`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ topic, type, start_time, duration, timezone }),
    });
    const meeting = await zoomRes.json();
    if (!zoomRes.ok) {
      return res.status(zoomRes.status).json({ error: meeting.message || 'Zoom API error', code: meeting.code });
    }
    res.status(200).json({
      id: meeting.id,
      join_url: meeting.join_url,
      start_url: meeting.start_url,
      topic: meeting.topic,
      start_time: meeting.start_time,
      duration: meeting.duration,
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** The Cloud Function deploys and the Firebase Console shows the function URL. A test POST to the function URL with topic, start_time, and duration returns a JSON object containing `join_url` and `id`.

### 3. Add a Zoom API Group and API Call in FlutterFlow

In your FlutterFlow project, click 'API Calls' in the left navigation panel. Click '+ Add' and select 'Create API Group'. Name it 'ZoomMeetings'. For the base URL, enter the root of your Firebase Cloud Function: `https://us-central1-your-project.cloudfunctions.net`. Add a `Content-Type: application/json` header. Now add an API Call inside the group: click '+ Add API Call', name it 'createMeeting', set the method to POST, and enter `/createZoomMeeting` as the endpoint path. Open the Variables tab and add the fields your function accepts: `topic` (String), `start_time` (String — ISO 8601 format, e.g., `2024-07-15T10:00:00`), `duration` (Integer, minutes), and `timezone` (String, e.g., `America/New_York`). In the Body tab, set the type to JSON and map the variables: `{ "topic": "{{ topic }}", "start_time": "{{ start_time }}", "duration": {{ duration }}, "timezone": "{{ timezone }}" }`. In the Response & Test tab, paste a sample response and generate JSON Paths for `$.join_url`, `$.id`, and `$.topic`. Test the API Call with real values to confirm a meeting is created in your Zoom account. Save the configuration.

```
{
  "topic": "{{ topic }}",
  "start_time": "{{ start_time }}",
  "duration": {{ duration }},
  "timezone": "{{ timezone }}"
}
```

**Expected result:** The API Call configuration is saved. A test call creates a real Zoom meeting and returns a valid `join_url`. The meeting appears in your Zoom account's Meetings list.

### 4. Display the Join URL and Trigger Meetings from App Actions

Now wire the Zoom API Call to a user interaction in your FlutterFlow app. Common patterns include a 'Schedule Meeting' form with topic, date picker, and duration fields, or a simple 'Start Instant Meeting' button. For the button approach: add a Button widget, open its Actions panel, and click '+ Add Action'. Choose 'Backend/API Call', select the 'ZoomMeetings' group, and pick the 'createMeeting' call. Map `topic` to a text field value or a static string, set `duration` to a number field, and set `start_time` to a formatted date value. After the API Call action, add a second action to store the response: either set a Page State variable `joinUrl` to the response JSON Path `$.join_url`, or write it to Firestore alongside a booking record. Display the join URL using a Text widget bound to the `joinUrl` page state variable, or use a 'Launch URL' action after the API Call to immediately open the join link in the device browser. If you're building a scheduling feature, save the `id`, `join_url`, and `start_time` to a Firestore collection like `meetings` so you can display upcoming meetings in a ListView.

**Expected result:** Tapping the 'Schedule Meeting' button creates a Zoom meeting, the join URL appears in the app, and the meeting record is saved to Firestore. The user can tap the URL to open Zoom.

### 5. (Optional) Pull Meeting Recordings and Participant Reports

After meetings end, Zoom provides cloud recording links and detailed participant reports — useful for building a dashboard in your FlutterFlow app where admins can review recordings and attendance. Add two more Cloud Functions: one for listing recordings (`GET /users/me/recordings?from={date}`) and one for participant reports (`GET /report/meetings/{meetingId}/participants`). Both reuse the same `getZoomToken()` helper from the create-meeting function. Important caveats: participant reports are only available 30–60 minutes after a meeting ends (and require `report:read:admin` scope), and cloud recordings require cloud recording to be enabled in the Zoom account settings. In FlutterFlow, add two new API Calls to your ZoomMeetings group pointing at these Cloud Function endpoints. Bind a ListView to the recordings response JSON Path `$.recording_files[*].download_url` and another to the participants response `$.participants[*].name`. For a dashboard that auto-updates, write the fetched data to Firestore after each meeting ends and bind the FlutterFlow list to that collection — more reliable than polling Zoom live.

```
// Add to functions/index.js — Zoom recordings endpoint
exports.getZoomRecordings = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
  const { from, userId = 'me' } = req.query;
  try {
    const token = await getZoomToken();
    const zoomRes = await fetch(
      `${ZOOM_API_BASE}/users/${userId}/recordings?from=${from || new Date().toISOString().slice(0, 10)}`,
      { headers: { Authorization: `Bearer ${token}` } }
    );
    const data = await zoomRes.json();
    res.status(200).json(data);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** The recordings endpoint returns a list of cloud recordings for the account. A FlutterFlow ListView displays recording titles, dates, and download links. The participant report endpoint returns attendee names and join/leave times.

## Best practices

- Never put Zoom Account ID, Client ID, or Client Secret in FlutterFlow API Call headers or Dart code — these credentials must live in Firebase Cloud Function environment config or Secret Manager.
- Cache the Zoom access token in memory or Firestore (55-minute window, not the full 3600 seconds) to avoid re-minting on every API call and hitting token endpoint rate limits.
- Use Server-to-Server OAuth exclusively — the legacy JWT app type was deprecated by Zoom in September 2023 and any tutorial still showing JWT tokens is outdated.
- Store the returned `join_url`, `id`, and `start_time` to Firestore after creating a meeting so the data persists if the user navigates away or the page state clears.
- Add scopes incrementally — only request the Zoom scopes you actually need (`meeting:write:admin` for create, `report:read:admin` for participant reports) to minimize the permissions footprint of your app.
- Test meeting creation in a staging Firebase project before deploying to production — Zoom meetings created in testing count against your account's meeting quota.
- For participant attendance dashboards, write report data to Firestore 90+ minutes after each meeting ends rather than fetching live — Zoom reports are not available immediately after meetings conclude.
- Validate date/time input in FlutterFlow before calling the API — ISO 8601 format (`2024-07-15T10:00:00`) with a valid timezone is required; malformed start_time values cause silent failures.

## Use cases

### Tutoring app that lets students book live Zoom sessions with instructors

A FlutterFlow edtech app lets students pick an available time slot and tap 'Book Session'. The app calls a Cloud Function that creates a Zoom meeting scheduled for that time and returns the join URL. The join link is saved to Firestore alongside the booking record and displayed to both the student and instructor in their respective dashboards.

Prompt example:

```
When a student books a session, create a Zoom meeting at the selected time and display the join link on the booking confirmation screen.
```

### CRM app with built-in meeting scheduling for sales calls

A FlutterFlow CRM app lets sales reps create instant Zoom meetings directly from a contact's record. Tapping 'Start Meeting' calls the Cloud Function, creates a Zoom meeting, saves the join URL to the contact's activity log in Firestore, and opens a share sheet so the rep can send the link immediately.

Prompt example:

```
Add a 'Create Zoom Meeting' button on each contact profile that generates an instant meeting link and logs it to the contact's activity history.
```

### Team app that displays past meeting recordings and attendance

A FlutterFlow internal tools app shows a list of completed Zoom meetings with clickable recording links and participant counts. The data is fetched from Zoom's `/recordings` and `/report/meetings` endpoints via Cloud Function calls and cached in Firestore. Team managers can review who attended which meeting from inside the app.

Prompt example:

```
Show a list of past Zoom meetings with recording links and attendance counts, fetched from the Zoom API and displayed in a scrollable dashboard.
```

## Troubleshooting

### 401 Unauthorized from the Cloud Function when calling Zoom API

Cause: The Zoom access token has expired (tokens last exactly 3600 seconds / 1 hour) and the cache has not been refreshed, or the Client ID/Secret stored in Firebase config do not match the Zoom Marketplace app credentials.

Solution: The Cloud Function code in Step 2 includes token caching with a 5-minute buffer — verify the `tokenExpiresAt` check is working. Run `firebase functions:config:get zoom` to confirm the stored credentials match the Zoom Marketplace app's Account ID, Client ID, and Client Secret. If credentials were recently rotated, update config and redeploy.

### Zoom API returns error code 300: 'Invalid access token'

Cause: The Zoom Server-to-Server OAuth app has not been activated in the Zoom Marketplace, or the scopes required for the operation have not been added to the app.

Solution: In the Zoom Marketplace, open your Server-to-Server OAuth app and verify its status shows 'Activated'. If it shows 'Created' or 'Development', click Activate. On the Scopes page, confirm the needed scopes (`meeting:write:admin`, `meeting:read:admin`) are listed. Re-authorize the app if you added scopes after initial activation.

### Participant report returns empty data immediately after the meeting ends

Cause: Zoom participant reports are not available in real-time — they take 30–60 minutes to generate after a meeting ends.

Solution: Do not poll the participant report endpoint immediately after a meeting. Schedule the Cloud Function call to run 90 minutes after the meeting's scheduled end time (using a Firebase scheduled function or a Firestore-triggered function with a delay). Display a 'Report generating...' state in the FlutterFlow UI until data is available.

### XMLHttpRequest error in FlutterFlow web preview when calling the Cloud Function

Cause: The Cloud Function is missing CORS headers, causing the browser to block the response in FlutterFlow's web Run Mode.

Solution: Add `res.set('Access-Control-Allow-Origin', '*')` and handle OPTIONS preflight requests at the top of each Cloud Function handler. The code in Step 2 already includes this — check that your deployed function version includes these lines.

```
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
```

### JWT-based Zoom integration stopped working

Cause: Zoom deprecated JWT app authentication in September 2023. JWT tokens issued by old Zoom JWT apps no longer work.

Solution: Create a new Server-to-Server OAuth app in the Zoom Marketplace (as described in Step 1) and update your Cloud Function to use the Account ID + Client ID + Client Secret credentials flow. If you'd prefer expert help migrating the integration, RapidDev's team handles FlutterFlow integrations every week — free scoping call at rapidevelopers.com/contact.

## Frequently asked questions

### Can I use Zoom's JWT authentication instead of Server-to-Server OAuth?

No — Zoom deprecated JWT app authentication in September 2023. JWT tokens no longer work. The correct replacement is Server-to-Server OAuth, which uses an Account ID, Client ID, and Client Secret to mint short-lived Bearer tokens. All new Zoom integrations must use Server-to-Server OAuth or user-level OAuth, depending on the use case.

### Does FlutterFlow need to display the Zoom meeting inside the app, or just share the link?

The simplest and most reliable approach is to display the join_url as a tappable link or button that opens the Zoom app (or Zoom web) on the user's device. Embedding a Zoom meeting directly inside a FlutterFlow app requires a Custom Widget using Zoom's native iOS/Android SDK, which is a significantly more complex integration involving platform-level code. For most use cases, sharing the join link is sufficient and far easier to maintain.

### How do I let individual users connect their own Zoom accounts (not just the admin account)?

Server-to-Server OAuth is for creating meetings on behalf of the account that registered the app — ideal for SaaS apps where your business hosts meetings. If individual users need to create meetings in their own personal Zoom accounts, you need user-level OAuth (a different Zoom app type), which requires users to authenticate with their Zoom credentials. This is a more complex flow involving the `authorization_code` grant type and per-user token storage in Firestore, similar to the Yahoo Mail OAuth pattern.

### Is there a limit to how many Zoom meetings I can create via the API?

Zoom's API rate limits are approximately 100 requests per second at the account level. Meeting creation limits depend on your Zoom plan — free accounts can host one meeting at a time, while paid plans support concurrent meetings. The API itself does not add additional creation limits beyond what the account plan supports. Check your Zoom plan's concurrent meeting limit if you expect high-volume meeting creation.

### Can I get real-time attendance data during an active Zoom meeting?

No — Zoom does not provide real-time attendance data through the REST API during a live meeting. The participant report endpoint (`/report/meetings/{id}/participants`) only populates 30–60 minutes after the meeting ends. For real-time participant counts, Zoom offers a Webhooks subscription (which your Cloud Function can receive and write to Firestore), but this is a separate, more complex setup.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/zoom
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/zoom
