# How to Integrate Zoom with V0

- Tool: V0
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: April 2026

## TL;DR

To integrate Zoom with V0 by Vercel, create a Next.js API route that calls the Zoom REST API using OAuth 2.0 Server-to-Server credentials stored as Vercel environment variables. V0 generates the meeting management UI; the API route creates and manages Zoom meetings server-side so your API credentials never reach the browser.

## Manage Zoom Meetings Programmatically in Your V0 Next.js App

Zoom's REST API lets you create and manage meetings, retrieve recordings, send notifications, and build custom scheduling workflows — all from your own application. For founders building productivity tools, client portals, tutoring platforms, or event management apps with V0, Zoom integration means you can create meeting links automatically when users book appointments, send calendar invites, and track attendance without users having to copy-paste Zoom links manually.

The Zoom API uses OAuth 2.0 for authentication. For server-to-server integrations (where your app acts on behalf of a Zoom account rather than individual users), Zoom recommends the Server-to-Server OAuth app type. This generates a Client ID and Client Secret that you exchange for an access token with each API call. The access token expires after one hour, so your API route needs to handle token refresh.

V0 generates the UI for your meeting scheduler — the form to create a new meeting, the list of upcoming meetings, and the join button. The Next.js API route handles all the Zoom API communication securely. This guide walks through the Server-to-Server OAuth flow, which is the most common integration pattern for SaaS apps managing Zoom meetings on behalf of their users.

## Before you start

- A Zoom account (Pro plan or higher recommended for API access to all features)
- A Zoom Server-to-Server OAuth app created at marketplace.zoom.us with Client ID, Client Secret, and Account ID
- The app must have the required scopes: meeting:write:admin, meeting:read:admin (at minimum)
- A V0 account at v0.dev and a Vercel account for deployment
- Basic familiarity with OAuth 2.0 and HTTP API patterns

## Step-by-step guide

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

Before writing any code, you need to create the Zoom app that will authorize your Next.js application to use the Zoom API. Go to marketplace.zoom.us and sign in with your Zoom account. Click 'Develop' in the top navigation, then 'Build App'. From the app types, select 'Server-to-Server OAuth' — this type is designed for backend integrations where your server acts on behalf of your Zoom account without needing individual users to authorize. Give your app a name like 'My Scheduling App'. After creation, Zoom will generate three values you need to save: Account ID, Client ID, and Client Secret. Copy all three immediately and store them securely — the Client Secret is only shown once in some Zoom versions. Next, configure the required API scopes. Click 'Scopes' in the left sidebar and add: meeting:write:admin (to create and delete meetings), meeting:read:admin (to list meetings), and user:read:admin (to get user info). Save the scopes. Finally, activate the app by clicking 'Activate your app' — the app must be active for the OAuth token endpoint to work. Note that Server-to-Server OAuth apps act on behalf of your Zoom account, not individual user accounts.

**Expected result:** Your Zoom Server-to-Server OAuth app is created and activated with the required scopes. You have Account ID, Client ID, and Client Secret values ready.

### 2. Generate the Meeting UI with V0

Open V0 at v0.dev and describe the meeting management interface you want to build. V0 works best when you describe the full data flow — mention that meeting creation should POST to /api/zoom/meetings and the meetings list should GET from /api/zoom/meetings. V0 will generate a React component with the form and list, plus a stub API route. The meeting creation form should collect the topic, start time, duration, timezone, and optionally a password and waiting room setting. The meetings list should display each meeting's topic, start time, join URL, and a delete button. V0 can also generate a copy-to-clipboard button for join URLs — ask for it explicitly in your prompt since it is a common UX pattern for meeting links. Once the design looks right, deploy from V0 to get a Vercel preview URL. You will configure real Zoom credentials in the next steps.

**Expected result:** V0 generates a meeting scheduler UI with form and table. The project is deployed to a Vercel preview URL showing placeholder data.

### 3. Build the Zoom API Route with OAuth Token Exchange

The core of the integration is the Next.js API route that handles Zoom API calls. Zoom's Server-to-Server OAuth requires exchanging your Account ID, Client ID, and Client Secret for a bearer access token at https://zoom.us/oauth/token. This token expires after one hour. For simplicity in a serverless context, request a new token with each API call rather than managing token refresh state — the token endpoint is fast and the overhead is minimal in a Vercel serverless function. Create app/api/zoom/meetings/route.ts with GET (list meetings) and POST (create meeting) handlers. The GET handler fetches the host user's ID first via the /users/me endpoint, then uses it to list meetings. The POST handler creates a scheduled meeting with the data from the request body. Both handlers call a shared getAccessToken() helper function that performs the OAuth token exchange. Note that the Authorization header for the token exchange uses Basic auth with base64-encoded credentials: btoa(clientId:clientSecret).

```
import { NextResponse } from 'next/server';

async function getAccessToken(): Promise<string> {
  const credentials = Buffer.from(
    `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
  ).toString('base64');

  const response = await fetch(
    `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`,
    {
      method: 'POST',
      headers: {
        Authorization: `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  const data = await response.json();
  if (!data.access_token) {
    throw new Error('Failed to get Zoom access token');
  }
  return data.access_token;
}

export async function GET() {
  try {
    const token = await getAccessToken();
    const meRes = await fetch('https://api.zoom.us/v2/users/me', {
      headers: { Authorization: `Bearer ${token}` },
    });
    const me = await meRes.json();

    const meetingsRes = await fetch(
      `https://api.zoom.us/v2/users/${me.id}/meetings?type=scheduled`,
      { headers: { Authorization: `Bearer ${token}` } }
    );
    const meetings = await meetingsRes.json();
    return NextResponse.json({ meetings: meetings.meetings ?? [] });
  } catch (error) {
    console.error('Zoom GET error:', error);
    return NextResponse.json({ error: 'Failed to fetch meetings' }, { status: 500 });
  }
}

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const token = await getAccessToken();
    const meRes = await fetch('https://api.zoom.us/v2/users/me', {
      headers: { Authorization: `Bearer ${token}` },
    });
    const me = await meRes.json();

    const response = await fetch(`https://api.zoom.us/v2/users/${me.id}/meetings`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        topic: body.topic,
        type: 2,
        start_time: body.startTime,
        duration: body.duration,
        timezone: body.timezone,
        password: body.password ?? '',
        settings: { waiting_room: true, host_video: true, participant_video: true },
      }),
    });

    const meeting = await response.json();
    if (!response.ok) {
      return NextResponse.json({ error: meeting.message }, { status: response.status });
    }
    return NextResponse.json({ meeting });
  } catch (error) {
    console.error('Zoom POST error:', error);
    return NextResponse.json({ error: 'Failed to create meeting' }, { status: 500 });
  }
}
```

**Expected result:** The API route handles GET and POST requests. Testing GET at /api/zoom/meetings returns the meetings array from your Zoom account.

### 4. Add Environment Variables in Vercel Dashboard

Configure your Zoom OAuth credentials in Vercel so the deployed API route can authenticate. Navigate to your project in the Vercel dashboard, go to Settings → Environment Variables, and add three variables. ZOOM_ACCOUNT_ID is the Account ID from your Zoom marketplace app (a string like AbCdEfGhIjKlMnOp). ZOOM_CLIENT_ID is the Client ID shown in your app's credentials page. ZOOM_CLIENT_SECRET is the Client Secret — treat this like a password and never commit it to Git. Set all three for the Production and Preview environments. If you want the Preview deployment to use a separate Zoom app for safety (so test meeting creation does not clutter your real Zoom account), create a second Zoom Server-to-Server OAuth app for development and use its credentials for the Preview environment scope. After adding all variables, go to the Deployments tab and click Redeploy to apply the new configuration. If you access the Vercel environment variables section and see the variables are grayed out after redeploying, the deployment has successfully picked them up — the values are masked for security.

```
# .env.local (local development only — set in Vercel Dashboard for deployments)
ZOOM_ACCOUNT_ID=your_account_id_here
ZOOM_CLIENT_ID=your_client_id_here
ZOOM_CLIENT_SECRET=your_client_secret_here
```

**Expected result:** All three environment variables are set in Vercel Dashboard. The latest deployment is redeployed and the API route can now successfully exchange credentials for Zoom access tokens.

### 5. Test the Integration and Handle Meeting Webhooks

Test the full integration by visiting your Vercel deployment URL and creating a meeting through the form. If creation succeeds, the meeting should appear in the table within seconds. Common failures at this stage include the Zoom app not being 'activated' (check the app status in Zoom Marketplace) or missing scopes (the error message will indicate which scope is missing). To handle Zoom webhooks — for example, receiving a notification when a meeting starts or ends — create an additional route at app/api/zoom/webhook/route.ts. Zoom webhooks require URL verification: when you register a webhook URL in the Zoom Marketplace app settings, Zoom sends a CRC validation request that your endpoint must respond to correctly. The validation involves computing an HMAC-SHA256 hash of the challenge token using your Webhook Secret Token. Once verified, Zoom will send event notifications to your endpoint for the events you subscribe to (meeting.started, meeting.ended, participant.joined, etc.). For RapidDev clients building complex scheduling platforms, we can help implement full webhook handling and database sync for meeting attendance tracking.

```
// app/api/zoom/webhook/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: Request) {
  const body = await request.json();

  // Handle Zoom URL validation challenge
  if (body.event === 'endpoint.url_validation') {
    const hash = crypto
      .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET!)
      .update(body.payload.plainToken)
      .digest('hex');
    return NextResponse.json({
      plainToken: body.payload.plainToken,
      encryptedToken: hash,
    });
  }

  // Handle meeting events
  if (body.event === 'meeting.started') {
    console.log('Meeting started:', body.payload.object.id);
  }
  if (body.event === 'meeting.ended') {
    console.log('Meeting ended:', body.payload.object.id);
  }

  return NextResponse.json({ received: true });
}
```

**Expected result:** Meeting creation and listing work end-to-end in the Vercel deployment. The webhook endpoint passes Zoom's URL validation challenge and receives meeting event notifications.

## Best practices

- Use Server-to-Server OAuth for backend integrations — never use personal access tokens or Zoom JWT tokens (deprecated) in new apps
- Cache Zoom access tokens with their expiry time (3600 seconds) to avoid making a token exchange request on every single API call
- Always include a waiting room and passcode on programmatically created meetings to prevent unauthorized access
- Validate webhook signatures using the HMAC-SHA256 challenge to ensure webhook payloads genuinely originate from Zoom
- Store only meeting IDs and join URLs in your database — avoid storing full Zoom API responses which can change structure between API versions
- Test webhook handling with Zoom's built-in webhook test feature in the Marketplace app settings before going live
- Handle the case where a meeting is deleted in Zoom directly (outside your app) by implementing a sync check in your meetings list fetch

## Use cases

### Automated Meeting Scheduler

When a user books an appointment in your app, automatically create a Zoom meeting and return the join URL. Combine with Calendly or your own booking form to create end-to-end scheduling that provisions the Zoom link without any manual steps.

Prompt example:

```
Create a meeting scheduler form with fields for meeting topic, date picker, time picker, and duration dropdown (30min/60min/90min). Add a 'Schedule Meeting' button that POSTs to /api/zoom/meetings and displays the generated join link and passcode in a success card.
```

### Meeting Management Dashboard

Build an internal dashboard that lists all upcoming scheduled Zoom meetings, shows participant counts, and lets admins start or delete meetings. Useful for operations teams managing webinars or client calls at scale.

Prompt example:

```
Build a meeting management dashboard with a table of upcoming Zoom meetings showing topic, host, scheduled time, and number of registrants. Include 'Start Meeting' and 'Delete' action buttons per row. Fetch from /api/zoom/meetings with a GET request on page load.
```

### Webinar Registration Page

Create a landing page where visitors can register for a Zoom webinar. The API route adds registrants to the Zoom webinar programmatically and sends them a confirmation with the personalized join link.

Prompt example:

```
Design a webinar registration page with the event title, description, date, and a registration form with name and email fields. The 'Register' button should POST to /api/zoom/register and show a confirmation message with the webinar join link returned by the API.
```

## Troubleshooting

### 401 Unauthorized: 'Invalid client_id or client_secret' when requesting access token

Cause: The ZOOM_CLIENT_ID or ZOOM_CLIENT_SECRET environment variable does not match the credentials in your Zoom Marketplace app, or the app is not activated.

Solution: Go to marketplace.zoom.us → Your App → App Credentials. Verify the Client ID and Client Secret match exactly. Ensure the app status is 'Activated'. If the Client Secret was regenerated, update ZOOM_CLIENT_SECRET in Vercel Dashboard and redeploy.

### 403 Forbidden: 'Insufficient scope' when calling the meetings endpoint

Cause: Your Zoom Server-to-Server OAuth app does not have the required API scopes enabled.

Solution: In Zoom Marketplace, go to your app → Scopes tab. Add meeting:write:admin and meeting:read:admin. Save changes. Note that scope changes on Server-to-Server OAuth apps take effect immediately without requiring a new token.

### Meeting is created but shows as 'Instant' type instead of 'Scheduled'

Cause: The start_time is in the wrong format or the meeting type is set to 1 (instant) instead of 2 (scheduled).

Solution: Ensure the start_time in the POST body is in ISO 8601 UTC format: 2026-04-15T14:00:00Z. Confirm the meeting type is 2 in the API route body. Zoom interprets missing or invalid start_time as an instant meeting request.

```
body: JSON.stringify({
  type: 2,  // 2 = Scheduled meeting
  start_time: new Date(body.startTime).toISOString(),
  timezone: body.timezone,
})
```

### Webhook validation fails — Zoom shows 'Unable to connect to endpoint'

Cause: The webhook URL is not publicly accessible (testing locally), the endpoint is returning the wrong response format, or the ZOOM_WEBHOOK_SECRET is incorrect.

Solution: Ensure you are using your Vercel deployment URL (not localhost) as the webhook endpoint. Verify the response includes both plainToken and encryptedToken fields with the correct HMAC-SHA256 computation. Check ZOOM_WEBHOOK_SECRET matches the Secret Token shown in Zoom Marketplace app → Feature → Event Subscriptions.

## Frequently asked questions

### Does the Zoom API integration work on a Zoom free plan?

Basic Zoom API access is available on free accounts, but most useful features like scheduled meetings over 40 minutes, API rate limits, and webinar APIs require a Pro plan ($149.90/year) or higher. For production apps, a Pro plan is strongly recommended.

### Can I create meetings on behalf of multiple Zoom users in my organization?

Yes. Server-to-Server OAuth with the meeting:write:admin scope lets you create meetings on behalf of any user in your Zoom account. Instead of calling /users/me, call /users/{userId}/meetings where userId is the email or Zoom user ID of the specific host. You must have Zoom admin access on your account.

### How do I get the meeting recording after it ends?

Subscribe to the recording.completed webhook event to receive a notification when a recording is available. The webhook payload includes download URLs for the video, audio, and transcript files. You can also poll GET /meetings/{meetingId}/recordings after the meeting ends using the cloud_recording:read:admin scope.

### What happens to Zoom meetings created by my app if the Zoom app is deactivated?

Existing meetings continue to function — Zoom meetings exist at the account level, not the API app level. Deactivating the Zoom Marketplace app only prevents your Next.js app from creating new meetings or fetching meeting data via the API. Already-scheduled meetings and their join links remain valid.

### Is there a rate limit on the Zoom API?

Yes. Zoom enforces rate limits per account: typically 100 requests per second for most endpoints on Pro plans. The /meetings endpoint has additional limits on meeting creation frequency. If you hit rate limits (HTTP 429), implement exponential backoff in your API route and consider caching GET requests for the meetings list.

---

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