Connect Bubble to Zoom using Server-to-Server OAuth — the only valid auth method since Zoom deprecated JWT in September 2023. A Backend Workflow exchanges your Account ID, Client ID, and Client Secret for a Bearer token, stores it with an expiry timestamp, and refreshes it automatically. All subsequent Zoom API calls pick up the cached token so your meetings are created securely server-side without exposing credentials to the browser.
| Fact | Value |
|---|---|
| Tool | Zoom |
| Category | Communication |
| Method | Bubble API Connector |
| Difficulty | Intermediate |
| Time required | 2–3 hours |
| Last updated | July 2026 |
Why Zoom requires Server-to-Server OAuth in Bubble
Zoom retired its legacy JWT app type in September 2023. Any tutorial still showing a static ZOOM_JWT_TOKEN is broken — those tokens simply stop working. The replacement is Server-to-Server OAuth: your Zoom app uses an Account ID, a Client ID, and a Client Secret to request short-lived Bearer tokens directly from Zoom's authentication server. In Bubble, this token exchange must live in a Backend Workflow (an API Workflow) rather than a client-side workflow, because the request uses Basic Auth credentials that must never appear in browser DevTools. Once a valid Bearer token is cached in your database with an expiry timestamp, every subsequent Zoom API call reads that token and refreshes it automatically when it's within five minutes of its one-hour TTL. The result is a secure, maintenance-free Zoom integration that creates meetings on behalf of your whole Zoom account.
Integration method
Two API Connector calls — one POST to Zoom's token endpoint and one POST to the meetings endpoint — both with credentials in Private headers, backed by Backend Workflows for token refresh.
Prerequisites
- A Bubble app on a paid plan (Starter or above) — Backend Workflows are not available on Bubble Free
- A Zoom account with admin access to the Zoom Marketplace (marketplace.zoom.us)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
Step-by-step guide
Create a Server-to-Server OAuth app in Zoom Marketplace
Open marketplace.zoom.us and sign in with your Zoom admin account. Click 'Develop' in the top navigation, then 'Build App.' Select 'Server-to-Server OAuth' from the app type list — do not choose 'OAuth' (user-level) or 'JWT' (deprecated). Give your app a name like 'Bubble Integration' and click 'Create.' On the app dashboard you will see your Account ID, Client ID, and Client Secret — copy all three and store them somewhere safe. Next, navigate to the 'Scopes' tab and add at minimum the scope `meeting:write:admin` (to create meetings) and `meeting:read:admin` (to retrieve meeting details). If you plan to pull participant reports, also add `report:read:admin`. Once scopes are saved, go to the 'Activation' tab and click 'Activate your app.' The app must be active before the token endpoint will return tokens. Activation typically requires agreeing to Zoom's API terms for Server-to-Server apps.
1// Scopes to add in Zoom Marketplace → Scopes tab:2meeting:write:admin3meeting:read:admin4report:read:admin // only if you need participant reportsPro tip: Keep your Client Secret private — treat it like a password. If it leaks, regenerate it immediately in the Zoom Marketplace app dashboard.
Expected result: Your Zoom Marketplace app shows 'Active' status and you have your Account ID, Client ID, and Client Secret ready to paste into Bubble.
Add the Zoom API Connector in Bubble with two calls
In your Bubble editor, click 'Plugins' in the left sidebar, then 'Add plugins.' Search for 'API Connector' (by Bubble), click 'Install,' and close the plugins panel. Now click 'Plugins' → 'API Connector.' Click 'Add another API' and name it 'Zoom.' Under this API you will add two separate calls. Call 1 — Token Exchange: Click 'Add another call.' Name it 'Get Access Token.' Set Method to POST and URL to `https://zoom.us/oauth/token`. Click 'Add header' and add `Content-Type` with value `application/x-www-form-urlencoded`. Now add the authentication: click 'Add parameter,' name it `grant_type`, set value to `account_credentials`, and leave 'Private' unchecked (this is a body param, not a secret). For authentication, Zoom's token endpoint requires Basic Auth where the username is your Client ID and the password is your Client Secret — NOT Base64-encoded manually; Bubble's built-in Basic Auth does the encoding for you. Click the authentication dropdown at the top of the call (default is 'None'), change it to 'Basic auth,' enter your Client ID as Username and Client Secret as Password, and check 'Private' on both. Add a second body param named `account_id` with your Account ID value — also mark it Private. Call 2 — Create Meeting: Click 'Add another call.' Name it 'Create Meeting.' Set Method to POST and URL to `https://api.zoom.us/v2/users/me/meetings`. Add a header: `Authorization` with value `Bearer <access_token>` — check 'Private' and change it to a dynamic parameter so you can pass the stored token at runtime. Add a header `Content-Type: application/json`. In the Body section select 'JSON body' and add your meeting fields with dynamic placeholders.
1// Call 1 — Token Exchange configuration2{3 "method": "POST",4 "url": "https://zoom.us/oauth/token",5 "headers": {6 "Content-Type": "application/x-www-form-urlencoded",7 "Authorization": "Basic <Base64(ClientID:ClientSecret)>" // Bubble sets this via Basic Auth — mark Private8 },9 "body": {10 "grant_type": "account_credentials",11 "account_id": "<your_account_id>" // mark Private12 }13}1415// Call 2 — Create Meeting configuration16{17 "method": "POST",18 "url": "https://api.zoom.us/v2/users/me/meetings",19 "headers": {20 "Authorization": "Bearer <dynamic_token>", // mark Private, set dynamically21 "Content-Type": "application/json"22 },23 "body": {24 "topic": "<dynamic_topic>",25 "type": 2,26 "start_time": "<dynamic_start_time>",27 "duration": "<dynamic_duration_minutes>",28 "settings": {29 "join_before_host": false,30 "waiting_room": true31 }32 }33}Pro tip: Bubble's 'Initialize call' for the Token Exchange requires a real successful response before you can use the call in workflows. After entering your credentials, click 'Initialize call' and confirm you see a response containing 'access_token'. If you see a 401 error, double-check that your app is Activated in Zoom Marketplace.
Expected result: Both API Connector calls are saved in Bubble. Clicking 'Initialize call' on the Token Exchange call returns a JSON response with an access_token field. The Create Meeting call shows the expected response fields including id, join_url, and start_url.
Create the Zoom_Tokens data type in Bubble
Go to your Bubble editor's 'Data' tab, then click 'Data types.' Click 'Add a data type' and name it `Zoom_Tokens`. Add two fields to this data type. The first field is `token` — set its type to 'text' — this will store the access_token string returned by Zoom's token endpoint. The second field is `expires_at` — set its type to 'date' — this will store the exact timestamp when the token expires so your Backend Workflow can check whether a refresh is needed before making API calls. You only need one record in this data type at any time (it gets updated in place), but it is useful to also add an `account_id` field (text) if you ever integrate multiple Zoom accounts. While you are in the Data tab, also go to 'Privacy' and add a privacy rule for the Zoom_Tokens data type: only your app's admin users or server-side workflows should be able to read token records. Regular end users should never see stored tokens — set the rule to 'Everyone else → no access' and use 'Ignore privacy rules when using workflows' for the Backend Workflows that need to read the token.
1// Zoom_Tokens data type fields:2// token → text3// expires_at → date4// account_id → text (optional, for multi-account setups)Pro tip: Privacy rules are critical. If you accidentally leave Zoom_Tokens readable to all users, anyone who knows to look in Bubble's Data API can read your access token. The token gives full meeting:write:admin access to your Zoom account.
Expected result: Zoom_Tokens data type exists in Bubble with token (text) and expires_at (date) fields, and a privacy rule prevents end users from reading these records.
Build the 'Refresh Zoom Token' Backend Workflow
Backend Workflows (API Workflows) are available only on Bubble paid plans — if you are on Bubble Free, upgrade before proceeding. In your Bubble editor, go to 'Backend Workflows' in the left sidebar. Click 'Add workflow' and name it `Refresh Zoom Token`. This workflow will: (1) call the Token Exchange API Connector call, (2) calculate the expiry timestamp, and (3) upsert the Zoom_Tokens record. Step-by-step within the workflow: 1. Add action: 'Plugins → API Connector - Zoom - Get Access Token.' This fires the token exchange call you configured in the previous step. 2. Add action: 'Data (Things) → Make changes to a list of Things' or 'Create a new Thing.' Search for an existing Zoom_Tokens record first. If one exists, update its `token` field to the result of step 1 (Result of step 1's body access_token) and update `expires_at` to 'Current date/time + 3600 seconds.' If no record exists, create a new Zoom_Tokens record with the same values. In practice, you can simplify this by always deleting the old record and creating a new one, or using Bubble's 'Do a search for' to find the existing record. 3. The workflow does not need to return anything — it is triggered by the 'Create Zoom Meeting' workflow (next step) when the token is stale.
1// Backend Workflow: Refresh Zoom Token2// Step 1: Call API Connector → Zoom → Get Access Token3// Step 2: Search for Zoom_Tokens (first item)4// IF found: Make changes → token = Step1's access_token5// expires_at = Current date/time + 3600 seconds6// IF not found: Create new Zoom_Tokens → token = Step1's access_token7// expires_at = Current date/time + 3600 secondsPro tip: Use 'Current date/time + 3540 seconds' (59 minutes) rather than 3600 seconds as expires_at. This gives a 60-second buffer before the token actually expires, preventing edge cases where a token appears valid but expires mid-request.
Expected result: Running the 'Refresh Zoom Token' Backend Workflow creates or updates a Zoom_Tokens record in your database with a valid access_token string and a future expires_at timestamp approximately 59 minutes from now.
Build the 'Create Zoom Meeting' Backend Workflow
Create a second Backend Workflow named `Create Zoom Meeting`. This workflow accepts input parameters — at minimum `topic` (text), `start_time` (date), and `duration_minutes` (number) — so the frontend can pass meeting details when triggering it. Inside the workflow: 1. Add a condition check: 'Do a search for Zoom_Tokens' — get the first result. Check if it exists AND if `expires_at > Current date/time`. If the token is missing or expired, add an action: 'Schedule API Workflow → Refresh Zoom Token' (call it synchronously using 'Run Backend Workflow' from the Workflow editor, or by making 'Refresh Zoom Token' a precondition step). In practice, the cleanest approach is to add the token-refresh logic as step 1 of this workflow with a condition: 'Only when Zoom_Tokens's first item's expires_at is before Current date/time OR Zoom_Tokens count is 0.' 2. After ensuring a valid token exists, add action: 'Plugins → API Connector - Zoom - Create Meeting.' In the dynamic header field for Authorization, reference `Do a search for Zoom_Tokens first item's token`. Pass the topic, start_time (formatted as ISO 8601: YYYY-MM-DDTHH:MM:SS), and duration from the workflow's input parameters. 3. Add action: save the meeting data. Create a new Meeting record (or update an existing booking record) with `join_url = Result of step 2 - join_url`, `start_url = Result of step 2 - start_url`, and `zoom_meeting_id = Result of step 2 - id`. 4. Return the join_url as the workflow's return value so the frontend can display it immediately after the workflow completes.
1// Backend Workflow: Create Zoom Meeting2// Input params: topic (text), start_time (date), duration_minutes (number)3//4// Step 1 (conditional): IF Zoom_Tokens is empty OR expires_at < Current date/time5// → Run Backend Workflow: Refresh Zoom Token6//7// Step 2: API Connector → Zoom → Create Meeting8// Authorization header: Bearer [Zoom_Tokens first item's token]9// Body:10// topic: [input topic]11// type: 212// start_time: [input start_time formatted ISO 8601]13// duration: [input duration_minutes]14//15// Step 3: Create new Meeting record16// join_url: [Step 2 result - join_url]17// start_url: [Step 2 result - start_url]18// zoom_meeting_id: [Step 2 result - id]19//20// Step 4: Return join_url to the triggering workflowPro tip: Zoom returns two important URLs: join_url (for attendees) and start_url (for the host). Beginners often show the start_url to everyone — but only the host should use start_url, and it contains a short-lived authentication token. Always save both separately and show the correct one to each user type.
Expected result: Triggering 'Create Zoom Meeting' from the frontend successfully creates a meeting in your Zoom account and saves the join_url and start_url to your Bubble database. The meeting appears in your Zoom dashboard under 'Upcoming Meetings.'
Wire the frontend button and display the meeting link
Now connect everything to your app's UI. On your Bubble page, design a simple meeting scheduler: add a date/time picker input, a text input for the meeting topic, and a number input or dropdown for duration in minutes. Add a 'Create Meeting' button. In the button's workflow, add a single action: 'Backend Workflows → Schedule API Workflow → Create Zoom Meeting,' passing the page's input values as the workflow's parameters. Set the scheduled time to 'Current date/time' so it runs immediately (not delayed). To display the result, add a text element with the placeholder 'Your meeting link will appear here.' After the Backend Workflow runs and creates the Meeting record, redirect the user to a confirmation page or use Bubble's 'Go to page' action with URL parameters carrying the join_url. Alternatively, if you store the Meeting record linked to the Current User, display the most recently created Meeting's join_url in a text element using 'Do a search for Meetings where host = Current User, sorted by Created Date descending, first item's join_url.' For the host view, show an additional 'Start Meeting' button that links to the start_url in a new tab. Label it clearly so the host knows this is their private host link. RapidDev's team has built dozens of Bubble apps with Zoom and similar integrations — if you want a free scoping call on architecture decisions, visit rapidevelopers.com/contact.
1// Frontend button workflow:2// Event: Button is clicked3// Action 1: Schedule API Workflow → Create Zoom Meeting4// topic: Input_Topic's value5// start_time: DateTimePicker_StartTime's value6// duration_minutes: Input_Duration's value7//8// After the workflow runs, display the Meeting record:9// Text element dynamic expression:10// Do a search for Meetings where Created by = Current User11// Sort by Created Date descending | First item | join_urlPro tip: Participant reports from the endpoint /report/meetings/{id}/participants require the report:read:admin scope and are only available 30–60 minutes after a meeting ends. Do not show a 'View Participants' button until the meeting's scheduled end time has passed.
Expected result: Clicking 'Create Meeting' on your Bubble page creates a real Zoom meeting, stores the links in your database, and displays the join_url to the user. The meeting host sees a separate 'Start Meeting' link pointing to the start_url.
Common use cases
On-demand video meeting scheduler
Let users book sessions directly inside your Bubble app. A button press triggers a Backend Workflow that creates a Zoom meeting and saves the join_url and start_url to the booking record, so both the host and attendees get the right link without ever leaving your platform.
When a user clicks 'Book Session', run a Backend Workflow that creates a Zoom meeting via the API Connector, saves the join_url and start_url to the Sessions data type, and shows the join_url to the user in a popup.
Copy this prompt to try it in Bubble
Automated webinar link generation for course platforms
Course or cohort platforms can generate unique Zoom meeting links when a student enrolls in a live session. The workflow fires on new enrollment, creates the meeting with the correct start time and duration, and emails the join_url automatically.
When a new Enrollment record is created, trigger a Backend Workflow that creates a Zoom meeting with start_time from the Course's scheduled_date field, then stores join_url in the Enrollment record and sends it to the student's email via SendGrid.
Copy this prompt to try it in Bubble
Client portal meeting dashboard
Display all upcoming Zoom meetings for a logged-in client by pulling from your Bubble database where join links are stored. Let clients join with a single click, and show meeting status in real time based on start_time comparisons.
Show a Repeating Group of Sessions records filtered by Current User = host and scheduled_date > Current date/time, displaying the join_url as a button labeled 'Join Meeting'.
Copy this prompt to try it in Bubble
Troubleshooting
API Connector Initialize call returns 401 Unauthorized for the token exchange
Cause: The most common causes are: the Zoom Marketplace app is not yet Activated, the Client ID or Client Secret was copied with extra whitespace, or the Basic Auth is configured incorrectly (e.g., the account_id was placed in the auth username field instead of the Client ID).
Solution: Go to marketplace.zoom.us → your app → Activation tab and confirm status shows 'Active.' Re-copy the Client ID and Client Secret carefully, checking for trailing spaces. In Bubble's API Connector, the Basic Auth username must be the Client ID and the password must be the Client Secret — not the Account ID. The Account ID goes in the body as the account_id parameter.
'There was an issue setting up your call' error when initializing the Create Meeting call
Cause: The Initialize call for the Create Meeting endpoint requires a valid Bearer token to return a successful response. If the token field in the Authorization header is empty or contains a placeholder string, Zoom returns a 401 which Bubble interprets as a setup error.
Solution: First run the Token Exchange call to get a valid access_token. Then temporarily paste that token value directly into the Authorization header of the Create Meeting call (as a hardcoded value, not dynamic) and run Initialize. Once initialized successfully, switch the header value back to a dynamic reference pointing to your Zoom_Tokens database field. Delete any hardcoded token values afterward.
Backend Workflow option is greyed out or missing in the workflow editor
Cause: Backend Workflows (API Workflows) are only available on Bubble paid plans. The Bubble Free plan does not include this feature, so the 'Backend Workflows' section does not appear.
Solution: Upgrade your Bubble app to the Starter plan or above. The entire server-side token refresh pattern — and keeping credentials out of the browser — depends on Backend Workflows. There is no secure workaround on Bubble Free for this particular integration.
Zoom meeting is created but join_url is the same every time, even for different meetings
Cause: This can happen if the meeting type is set to 1 (Instant meeting, which always uses the host's Personal Meeting ID) instead of 2 (Scheduled meeting, which generates a unique ID per meeting).
Solution: In the Create Meeting API Connector call body, ensure `"type": 2` is set. Type 1 reuses the host PMI link. Type 2 creates a unique meeting with its own join_url and ID.
Best practices
- Always store the Zoom access_token in a Bubble data type with an expires_at field — never hardcode it in the API Connector or re-request a token on every API call. Token exchange requests count toward Zoom's rate limits and add unnecessary latency.
- Mark the Authorization header 'Private' in every Bubble API Connector call that sends the Bearer token. Without the Private checkbox, the token appears in browser network requests where any user can read it via DevTools.
- Apply Bubble privacy rules to the Zoom_Tokens data type so only server-side workflows can read token records. Regular users should have no access to this data type.
- Cache the join_url and start_url in your Bubble database when a meeting is created. Do not call the Zoom API again just to display the link — read it from your database instead to save WU and reduce API calls.
- Use `type: 2` (Scheduled meeting) instead of `type: 1` (Instant meeting with PMI) unless you deliberately want all meetings to share the same join link. Type 2 generates unique links per meeting.
- Show the start_url only to the meeting host. The start_url contains a short-lived authentication token that logs the user in as the host — sharing it accidentally gives a participant host controls.
- Add a 60-second safety buffer when calculating token expiry: use `Current date/time + 3540 seconds` rather than 3600. This prevents edge cases where a token appears valid in your database but expires during the API call.
Alternatives
Google Meet has no standalone meeting API — Meet links are created as Google Calendar events with conferenceData. If your users are in the Google Workspace ecosystem and already authenticate with Google, Meet integration via the Calendar API (OAuth2 User-Agent in Bubble) is simpler. Zoom's Server-to-Server OAuth is better for admin-level meeting creation without requiring each user to authenticate individually.
Microsoft Teams meeting links require either the Graph API (complex Azure AD app registration with admin consent) or the simpler Workflows-app webhook for outbound notifications. For full meeting scheduling via Bubble, Zoom's API surface is significantly more developer-friendly. Choose Teams if your organization already mandates Microsoft 365.
Webex Events is a full virtual conference platform (multi-session scheduling, networking lounges, sponsor booths) rather than a simple video call scheduler. Use Webex Events if you are building a multi-session conference portal; use Zoom for straightforward one-on-one or small-group meeting creation.
Frequently asked questions
Can I use Zoom on Bubble's Free plan?
Partially. You can add the API Connector and make client-side Zoom API calls on the Free plan, but the secure Server-to-Server OAuth token refresh pattern requires Backend Workflows, which are only available on Bubble Starter and above. Without Backend Workflows you cannot safely cache and refresh the access token server-side, which means credentials are more exposed.
Why is Zoom's old JWT token no longer working?
Zoom deprecated the JWT app type in September 2023 and fully disabled existing JWT credentials. Any integration that stored a static ZOOM_JWT_TOKEN stopped working after the cutoff date. The replacement is Server-to-Server OAuth, which uses short-lived access tokens (1-hour TTL) obtained via a Client ID and Client Secret — far more secure than a static JWT.
How do I let users join with a link vs. launching the Zoom app?
Zoom's join_url always opens in the browser first and gives the user the option to launch the app or join via browser. You do not need to do anything special — just link to the join_url. The user's device and Zoom preferences determine whether the app opens automatically.
Can I schedule recurring meetings through the Bubble integration?
Yes. Set the meeting body's `type` to 8 (Recurring meeting, no fixed time) or 3 (Recurring meeting with fixed times) and include a `recurrence` object with the interval, frequency (daily/weekly/monthly), and end conditions. The API Connector call body supports nested JSON so you can pass the full recurrence configuration.
What happens if my Zoom token expires in the middle of a workflow?
If you implement the expiry check and refresh logic correctly — checking `expires_at` before each meeting-creation call — the workflow refreshes the token and retries seamlessly. If you skip the expiry check and the token has expired, Zoom returns a 401 error and Bubble logs it in the Logs tab → Workflow logs. Build the condition check as described in step 5 to avoid this.
Do participant reports work immediately after a meeting ends?
No. Participant reports from the /report/meetings/{id}/participants endpoint take 30–60 minutes to become available after a meeting ends. If you query this endpoint immediately after the meeting, Zoom returns an empty list or a 'not found' error. Build a delayed Backend Workflow scheduled 90 minutes after the meeting's start_time + duration to reliably fetch participant data.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation