Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Zoom

Integrate Bolt.new with Zoom by creating a Server-to-Server OAuth app at marketplace.zoom.us, storing credentials in your .env file, and calling the Zoom REST API through a Next.js API route to create meetings, generate join URLs, and access recordings. Outbound API calls work in Bolt's development preview; Zoom webhooks require a deployed URL since Bolt's WebContainer cannot receive incoming connections.

What you'll learn

  • How to create a Zoom Server-to-Server OAuth app in the Zoom Marketplace and get credentials
  • How to generate Zoom OAuth access tokens from your server-side API route
  • How to create Zoom meetings via the API and return join URLs to users
  • How to list past recordings and generate temporary access links for video playback
  • How to register and verify Zoom webhooks after deploying your app
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read35 minutesCommunicationApril 2026RapidDev Engineering Team
TL;DR

Integrate Bolt.new with Zoom by creating a Server-to-Server OAuth app at marketplace.zoom.us, storing credentials in your .env file, and calling the Zoom REST API through a Next.js API route to create meetings, generate join URLs, and access recordings. Outbound API calls work in Bolt's development preview; Zoom webhooks require a deployed URL since Bolt's WebContainer cannot receive incoming connections.

Embedding Zoom Meeting Functionality in Bolt.new Apps

The Zoom REST API lets you programmatically create meetings, manage participants, access cloud recordings, and monitor meeting activity — all without users needing to interact with the Zoom client directly. For Bolt.new apps, the most common use cases are scheduling meetings from within a custom app interface, displaying upcoming and past meetings in a dashboard, and providing one-click join buttons that skip the Zoom website.

Zoom offers several app types in its Marketplace. For server-side operations like creating meetings on behalf of your organization, the Server-to-Server OAuth app type is the right choice — it uses client credentials to get an access token without requiring any user to log in through Zoom. This is simpler to implement than user-level OAuth and works well for tools where one Zoom account hosts meetings for all users of your app. For cases where individual users need to manage their own meetings through your app, user-level OAuth is available but requires a deployed redirect URI.

Zoom enforces a rate limit of 10 requests per second across the REST API for most endpoints. For typical meeting scheduling and listing use cases this is generous. Webhook delivery from Zoom to your app — for events like meeting started, participant joined, or recording completed — requires your app to have a publicly accessible HTTPS endpoint, which means you must deploy to Netlify or Vercel before registering webhooks. Outbound API calls for creating meetings and fetching data work fine in Bolt's development preview.

Integration method

Bolt Chat + API Route

Bolt generates the Zoom integration code through conversation — you describe what meeting features you need and Bolt writes the API routes and React components. The Zoom Server-to-Server OAuth app type gives you a long-lived access token without user login, making it ideal for back-end meeting creation and management. All Zoom API calls go through server-side Next.js routes to keep your Account ID and Client Secret out of the browser. Zoom webhooks for real-time event notifications require a deployed public URL.

Prerequisites

  • A Zoom account with admin privileges (required to create Marketplace apps and enable Server-to-Server OAuth)
  • A Server-to-Server OAuth app created at marketplace.zoom.us with the correct scopes enabled
  • Account ID, Client ID, and Client Secret from your Zoom Marketplace app
  • A Next.js project in Bolt.new (prompt 'Create a Next.js app' to scaffold one)
  • For webhook testing: a deployed app on Netlify or Vercel with a public HTTPS URL

Step-by-step guide

1

Create a Zoom Server-to-Server OAuth app and configure scopes

Navigate to marketplace.zoom.us and sign in with your Zoom admin account. Click Develop → Build App in the top navigation, then select Server-to-Server OAuth from the app type list and click Create. Give your app a name and click Create again. You will see your App Credentials immediately: Account ID, Client ID, and Client Secret. Copy all three — you will need them for your .env file. The Client Secret is shown in full on this screen and cannot be retrieved again after you navigate away (though you can always generate a new one). Next, configure the required scopes. Scopes determine which Zoom API endpoints your app can call. Click the Scopes tab. For meeting management, add: meeting:read:admin (read meetings for all users), meeting:write:admin (create and update meetings for all users), and recording:read:admin (access cloud recordings). For participant reports, add report:read:admin. Click Save. Finally, activate the app by clicking the Activation tab and toggling the app to Active. Server-to-Server OAuth apps do not go through Zoom's app review process, so activation is immediate. Store the three credential values in your Bolt project's .env.local file as shown below.

Bolt.new Prompt

Create a Next.js app with a Zoom integration. Add a .env.local with ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, and ZOOM_CLIENT_SECRET placeholder variables. Create a utility file at lib/zoom.ts that exports a getZoomToken() function that fetches an OAuth access token from https://zoom.us/oauth/token using client credentials (grant_type: account_credentials) and caches it in memory with its expiry time.

Paste this in Bolt.new chat

.env.local
1// .env.local
2ZOOM_ACCOUNT_ID=your_account_id_here
3ZOOM_CLIENT_ID=your_client_id_here
4ZOOM_CLIENT_SECRET=your_client_secret_here

Pro tip: Server-to-Server OAuth app tokens expire after one hour. The getZoomToken() utility should cache the token in memory and only request a new one when the cached token is within 5 minutes of expiry.

Expected result: Your Zoom app appears as Active in the Marketplace, and the three credential values are stored in your .env.local file.

2

Implement the OAuth token utility and create meetings

The Zoom Server-to-Server OAuth token is obtained by making a POST request to https://zoom.us/oauth/token with grant_type=account_credentials and your Account ID in the URL. The request uses HTTP Basic authentication with your Client ID as the username and Client Secret as the password. The token response includes access_token and expires_in (3600 seconds). Cache the token and refresh it before it expires rather than requesting a new one on every API call. With a valid token, you can create meetings by POSTing to https://api.zoom.us/v2/users/{userId}/meetings where userId is either the Zoom user email of the host or 'me' for the authenticated user. The request body includes the meeting topic, type (1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time), start_time (ISO 8601 in UTC), duration (minutes), and settings like waiting_room and auto_recording. The response includes join_url (the link for participants), start_url (the link for the host to start the meeting), id (the meeting ID, 9-11 digit number), and password. The join_url is what you display to users — clicking it opens Zoom or the Zoom web client directly without requiring a Zoom account (depending on your Zoom plan settings). Because this is an outbound HTTPS request from your server-side API route, it works perfectly in Bolt's WebContainer development preview. You can test meeting creation, listing, and retrieval without deploying.

Bolt.new Prompt

Build the Zoom token utility and meeting creation API route. Create lib/zoom.ts with a cached getZoomToken function that authenticates with Server-to-Server OAuth. Create app/api/zoom/meetings/create/route.ts that accepts POST with topic, startTime (ISO 8601), durationMinutes, and hostEmail, creates the Zoom meeting, and returns the meetingId, joinUrl, startUrl, and password.

Paste this in Bolt.new chat

lib/zoom.ts
1// lib/zoom.ts
2let cachedToken: { token: string; expiresAt: number } | null = null;
3
4export async function getZoomToken(): Promise<string> {
5 if (cachedToken && Date.now() < cachedToken.expiresAt - 60_000) {
6 return cachedToken.token;
7 }
8
9 const accountId = process.env.ZOOM_ACCOUNT_ID!;
10 const clientId = process.env.ZOOM_CLIENT_ID!;
11 const clientSecret = process.env.ZOOM_CLIENT_SECRET!;
12 const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
13
14 const response = await fetch(
15 `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${accountId}`,
16 {
17 method: 'POST',
18 headers: { Authorization: `Basic ${credentials}` },
19 }
20 );
21
22 if (!response.ok) {
23 throw new Error(`Zoom token request failed: ${response.status}`);
24 }
25
26 const data = await response.json();
27 cachedToken = {
28 token: data.access_token,
29 expiresAt: Date.now() + data.expires_in * 1000,
30 };
31 return cachedToken.token;
32}
33
34// app/api/zoom/meetings/create/route.ts
35import { NextRequest, NextResponse } from 'next/server';
36import { getZoomToken } from '@/lib/zoom';
37
38export async function POST(request: NextRequest) {
39 const { topic, startTime, durationMinutes, hostEmail } = await request.json();
40 const token = await getZoomToken();
41
42 const response = await fetch(
43 `https://api.zoom.us/v2/users/${hostEmail ?? 'me'}/meetings`,
44 {
45 method: 'POST',
46 headers: {
47 Authorization: `Bearer ${token}`,
48 'Content-Type': 'application/json',
49 },
50 body: JSON.stringify({
51 topic,
52 type: 2, // Scheduled meeting
53 start_time: startTime,
54 duration: durationMinutes ?? 60,
55 timezone: 'UTC',
56 settings: {
57 waiting_room: false,
58 join_before_host: true,
59 auto_recording: 'none',
60 },
61 }),
62 }
63 );
64
65 if (!response.ok) {
66 const error = await response.json();
67 return NextResponse.json({ error }, { status: response.status });
68 }
69
70 const meeting = await response.json();
71 return NextResponse.json({
72 meetingId: meeting.id,
73 joinUrl: meeting.join_url,
74 startUrl: meeting.start_url,
75 password: meeting.password,
76 startTime: meeting.start_time,
77 });
78}

Pro tip: Use type: 2 for scheduled meetings that appear on participants' Zoom calendars. Use type: 1 for instant meetings that start immediately with no fixed time — these expire after 30 days.

Expected result: Calling /api/zoom/meetings/create with a valid topic and start time returns a JSON object containing the meeting ID, join URL, and host start URL.

3

List meetings and access cloud recordings

To display a list of upcoming or past meetings, call GET https://api.zoom.us/v2/users/{userId}/meetings with type=upcoming for scheduled meetings or type=previous_meetings for past ones. Add the page_size parameter (max 300) to control the number of results. The response includes an array of meeting objects with id, topic, start_time, duration, and status. For cloud recordings, call GET https://api.zoom.us/v2/users/{userId}/recordings with from and to date parameters in YYYY-MM-DD format. The response contains a meetings array where each meeting has a recording_files array. Each recording file has a download_url (requires a download_token in the Authorization header) and a play_url (for browser playback). The recording token is a separate authentication mechanism from the API access token — it is returned as download_token in the recording response and is valid for 24 hours. For participant reports (attendance data), first wait until the meeting ends (check status === 'ended'), then call GET https://api.zoom.us/v2/report/meetings/{meetingId}/participants. Note that participant reports are only available for meetings that occurred within the past 12 months and require the report:read:admin scope. There is typically a 30-minute delay between a meeting ending and the participant report being available.

Bolt.new Prompt

Add Zoom recordings listing to my app. Create /api/zoom/recordings route that fetches cloud recordings for the last 30 days from the Zoom API. For each recording, return: meeting topic, recording date, total size, and a play URL. Build a recordings list component that displays this data in a table with a 'Watch' link for each recording.

Paste this in Bolt.new chat

app/api/zoom/recordings/route.ts
1// app/api/zoom/recordings/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getZoomToken } from '@/lib/zoom';
4
5export async function GET(request: NextRequest) {
6 const { searchParams } = new URL(request.url);
7 const userId = searchParams.get('userId') ?? 'me';
8
9 // Default: last 30 days
10 const to = new Date().toISOString().split('T')[0];
11 const fromDate = new Date();
12 fromDate.setDate(fromDate.getDate() - 30);
13 const from = fromDate.toISOString().split('T')[0];
14
15 const token = await getZoomToken();
16
17 const response = await fetch(
18 `https://api.zoom.us/v2/users/${userId}/recordings?from=${from}&to=${to}&page_size=50`,
19 { headers: { Authorization: `Bearer ${token}` } }
20 );
21
22 if (!response.ok) {
23 return NextResponse.json({ error: `Zoom API error: ${response.status}` }, { status: response.status });
24 }
25
26 const data = await response.json();
27
28 const recordings = (data.meetings ?? []).map((meeting: Record<string, unknown>) => ({
29 meetingId: meeting.id,
30 topic: meeting.topic,
31 startTime: meeting.start_time,
32 duration: meeting.duration,
33 totalSize: meeting.total_size,
34 files: (meeting.recording_files as Array<Record<string, unknown>>)
35 ?.filter((f) => f.file_type === 'MP4')
36 .map((f) => ({
37 id: f.id,
38 playUrl: f.play_url,
39 downloadUrl: f.download_url,
40 fileSize: f.file_size,
41 })),
42 }));
43
44 return NextResponse.json({ recordings, totalRecords: data.total_records });
45}

Pro tip: Zoom recording play URLs use Zoom's own viewer page and work without authentication. Download URLs require the download_token as a URL parameter — append it as ?access_token={downloadToken} to the download_url.

Expected result: Calling /api/zoom/recordings returns a list of past meeting recordings with play URLs that open in Zoom's recording viewer.

4

Set up Zoom webhooks after deployment (for real-time events)

Webhooks let Zoom push real-time events to your app: meeting started, meeting ended, participant joined, participant left, recording completed. This is more efficient than polling and enables instant UI updates when meetings change status. However, Zoom webhooks require a publicly accessible HTTPS URL to send events to — Bolt's WebContainer cannot receive incoming HTTP connections, so you must deploy your app first. Deploy to Netlify or Vercel following the standard Bolt export workflow. Once you have a public URL (e.g., https://your-app.netlify.app), go back to your Zoom Marketplace app at marketplace.zoom.us → your app → Feature tab → Event Subscriptions. Enable Event Subscriptions, set the Event notification endpoint URL to https://your-app.netlify.app/api/zoom/webhook, and click Validate. Zoom will immediately send a challenge request to verify your endpoint — your API route must respond to this within 3 seconds. After validation, select the events you want: Meeting → Meeting has ended, Meeting → Recording completed, etc. Save the configuration. Zoom will now POST events to your webhook URL with a JSON payload describing what happened. Always verify the webhook signature using the x-zm-signature header and the Webhook Secret Token (shown in your Event Subscriptions configuration) to ensure requests are genuinely from Zoom and not from a third party.

Bolt.new Prompt

Create a Zoom webhook handler at /api/zoom/webhook. It should verify the webhook signature using the x-zm-signature header and ZOOM_WEBHOOK_SECRET environment variable. Handle the URL validation challenge (event: 'endpoint.url_validation') by returning the encrypted plainToken. For meeting.ended events, log the meeting ID and end time. Return 200 OK for all valid events.

Paste this in Bolt.new chat

app/api/zoom/webhook/route.ts
1// app/api/zoom/webhook/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import crypto from 'crypto';
4
5const WEBHOOK_SECRET = process.env.ZOOM_WEBHOOK_SECRET!;
6
7function verifyZoomWebhook(request: NextRequest, body: string): boolean {
8 const timestamp = request.headers.get('x-zm-request-timestamp');
9 const signature = request.headers.get('x-zm-signature');
10 if (!timestamp || !signature) return false;
11
12 const message = `v0:${timestamp}:${body}`;
13 const hash = crypto.createHmac('sha256', WEBHOOK_SECRET).update(message).digest('hex');
14 const expectedSignature = `v0=${hash}`;
15 return crypto.timingSafeEqual(
16 Buffer.from(signature),
17 Buffer.from(expectedSignature)
18 );
19}
20
21export async function POST(request: NextRequest) {
22 const rawBody = await request.text();
23
24 if (!verifyZoomWebhook(request, rawBody)) {
25 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
26 }
27
28 const payload = JSON.parse(rawBody);
29
30 // Handle URL validation challenge
31 if (payload.event === 'endpoint.url_validation') {
32 const hash = crypto
33 .createHmac('sha256', WEBHOOK_SECRET)
34 .update(payload.payload.plainToken)
35 .digest('hex');
36 return NextResponse.json({
37 plainToken: payload.payload.plainToken,
38 encryptedToken: hash,
39 });
40 }
41
42 // Handle meeting ended event
43 if (payload.event === 'meeting.ended') {
44 const { id, topic, end_time } = payload.payload.object;
45 console.log(`Meeting ended: ${id} (${topic}) at ${end_time}`);
46 // Add your database update or notification logic here
47 }
48
49 return NextResponse.json({ received: true });
50}

Pro tip: Add ZOOM_WEBHOOK_SECRET to your hosting platform's environment variables (Netlify: Site settings → Environment variables, Vercel: Project settings → Environment variables) before testing webhooks on your deployed URL.

Expected result: After deploying and registering the webhook, Zoom shows a green 'Validated' status for your endpoint URL. Meeting.ended events appear in your server logs when test meetings finish.

Common use cases

One-click meeting scheduler

Add a 'Schedule Zoom Call' button to your app that creates a meeting with the correct topic, duration, and time based on a form or booking selection, then returns a direct join URL displayed as a button. Users never need to leave your app to set up the meeting in Zoom.

Bolt.new Prompt

Create a meeting scheduler page in Next.js with a form for meeting topic, date/time, and duration. When submitted, call /api/zoom/meetings/create to create a Zoom meeting and display the returned join URL as a 'Join Meeting' button. Also show the host key and meeting ID.

Copy this prompt to try it in Bolt.new

Recording library dashboard

Build a searchable library of past Zoom cloud recordings for your team. Users can browse recordings by date, search by meeting topic, and play recordings directly in the browser using Zoom's temporary download links — no Zoom account required for viewers.

Bolt.new Prompt

Build a Zoom recordings page that fetches all cloud recordings for the last 30 days from the Zoom API. Display them in a grid with meeting topic, date, duration, and a 'Watch Recording' link. Use Zoom's recording download token to create a temporary playback link.

Copy this prompt to try it in Bolt.new

Meeting attendance tracker

After a scheduled meeting ends, automatically fetch the participant report from Zoom and store attendance data in your database. Display attendance history per user with join time, leave time, and duration for compliance or billing records.

Bolt.new Prompt

Create a /api/zoom/meetings/[meetingId]/participants route that fetches the participant report for a completed Zoom meeting. Store each participant's name, join time, leave time, and duration to my database. Build an attendance table component that displays this data.

Copy this prompt to try it in Bolt.new

Troubleshooting

API calls return 401 with 'Invalid access token' even immediately after getting a token

Cause: The Server-to-Server OAuth app is not activated, or the scope required for the API endpoint was not added before activation. Zoom checks scopes at token issuance — adding scopes after the app was already activated does not update existing tokens.

Solution: Go to marketplace.zoom.us → your app → Scopes tab and verify all required scopes are listed. Then go to the Activation tab and toggle the app off and back on to force a re-activation. After reactivation, fetch a fresh token by clearing the memory cache in your getZoomToken function or restarting the development server.

Webhook endpoint shows 'Validation failed' in Zoom Marketplace after entering the URL

Cause: The webhook route is not deployed (it is running only in Bolt's WebContainer, which is inaccessible from Zoom's servers), or the validation challenge handler is not returning the correct encryptedToken response format within the 3-second timeout.

Solution: Deploy your app to Netlify or Vercel first — Zoom webhooks cannot reach the Bolt preview URL. Confirm the deployed /api/zoom/webhook route is accessible by visiting it in a browser (it should return a method not allowed response, not a 404). Verify your validation handler returns both plainToken and encryptedToken fields as shown in the code example.

Meeting creation returns 400 with 'User does not exist' when using hostEmail

Cause: The email address provided as hostEmail is not a member of your Zoom account, or the Server-to-Server OAuth app does not have permission to create meetings on behalf of that user.

Solution: Use 'me' as the userId to create meetings under the app account owner, or fetch the list of users in your account with GET https://api.zoom.us/v2/users and use an email address from that list. The user must be a Licensed Zoom user (not a Basic/free user) to create scheduled meetings.

Cloud recordings return an empty meetings array even though recordings exist in the Zoom portal

Cause: The date range specified in the from and to parameters does not overlap with the recording dates, or the user ID used for the request is different from the user who hosted the recorded meetings.

Solution: Verify the from and to dates in your API request using YYYY-MM-DD format in the user's local timezone (not UTC). Check that the Zoom user email matches the account that hosts the meetings. Ensure the recording:read:admin scope is enabled on the app — without it, the recordings endpoint returns empty results silently.

Best practices

  • Use Server-to-Server OAuth instead of JWT authentication — Zoom deprecated JWT apps in June 2023, and all new integrations must use OAuth. Server-to-Server OAuth is the simplest OAuth type for server-side apps.
  • Cache the OAuth access token in memory with its expiry timestamp rather than requesting a new token for every API call — tokens last one hour and excessive token requests can trigger rate limiting.
  • Always verify webhook signatures using HMAC-SHA256 before processing any webhook payload — this prevents attackers from sending fake Zoom events to your endpoint.
  • Store ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET, and ZOOM_WEBHOOK_SECRET as server-side environment variables, never in client-side code or variables prefixed with NEXT_PUBLIC_.
  • Test meeting creation and listing in Bolt's development preview (outbound calls work fine), but register webhooks only after deploying — Zoom's validation challenge cannot reach the WebContainer.
  • Add error handling for Zoom's 429 rate limit response — the Retry-After header indicates when to retry. For meeting creation workflows, a maximum of 10 concurrent requests per second is rarely exceeded by normal usage.
  • Use Zoom's meeting password or waiting room settings appropriately for your security requirements — disable waiting_room and enable join_before_host for low-friction internal meetings, but keep waiting_room enabled for public-facing webinars.

Alternatives

Frequently asked questions

Can I test the Zoom integration in Bolt's preview without deploying?

Yes, for outbound API calls. Creating meetings, listing recordings, fetching meeting details, and getting participant reports all work from Bolt's WebContainer since they are outbound HTTPS requests. The one exception is webhooks — Zoom needs to send HTTP events to your server, which requires a deployed public URL. If you only need to create and read meetings, you can fully test in the Bolt preview.

What is the difference between JWT and Server-to-Server OAuth for Zoom?

Zoom deprecated JWT apps in June 2023 — do not use JWT for new integrations. Server-to-Server OAuth is the current recommended approach for backend applications. It works similarly to JWT in that no user login is required, but it uses the OAuth 2.0 client credentials flow with scoped permissions. Server-to-Server OAuth apps go through an immediate self-activation with no app review required.

Does Bolt.new have a native Zoom integration?

No. Zoom is not one of Bolt's native connectors. You build the integration manually using Next.js API routes and the Zoom REST API. Bolt's AI can generate most of the code when you describe what meeting features you need, but you must register your own Zoom Marketplace app and manage credentials yourself.

Can I start a Zoom meeting from my app without the host needing to open Zoom?

The start_url returned by the meeting creation API is a special host-only link that opens the Zoom desktop client for the host and starts the meeting. This link contains embedded authentication and expires after 90 days. You can display this to the meeting host. For a fully browser-based experience without the Zoom client, the Zoom Video SDK (a separate product) enables embedded in-browser video — it has different pricing and setup requirements.

How do I handle Zoom's 10 requests per second rate limit?

For normal meeting dashboard operations (loading a list of upcoming meetings, fetching recordings), 10 req/s is rarely a constraint. If you are processing many meetings in a loop — for example, fetching participant reports for 50 meetings at once — add a 100ms delay between requests to stay under the limit. Zoom returns a 429 status code with a Retry-After header when you exceed the rate limit; implement retry logic that reads this header.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.