Embed a Zoom meeting in your website

Embed a live Zoom meeting inside your own page layout with the Meeting SDK Component View. A customer joins a scheduled appointment from your site in their browser, with no download, no Zoom account, and no meeting number to type.

Updated August 2026

Meeting SDK

Node · Express · React · Vite

An appointment page embeds a live Zoom meeting directly in your own layout using the Meeting SDK  Component View. Your header, sidebar, and booking details stay on screen; the meeting renders into an element you control.

Sending a customer a Zoom link takes them off your site, often into a download prompt, sometimes into a sign-in wall. Every step between "I'm ready" and "I'm in the meeting" loses people. Embedding removes all of them: the visitor clicks once on the page they are already on.

What you'll need:

  • A Meeting SDK app in the Zoom Marketplace  for the SDK Key and Secret (free to create on any Zoom account; see Meeting SDK authorization )
  • The @zoom/meetingsdk  package (6.2.0 at the time of writing, MIT). It declares an exact peer dependency on react@18.2.0, so a React 19 project will not install it.
  • A backend that can sign an HS256 JWT (Node/Express in this guide; any stack works)
  • A scheduled meeting for the appointment, plus its meeting number and passcode
  • A browser that supports WebAssembly, which every current browser does

Features:

  • Meeting renders inside your page, next to your own content, not in place of it
  • One-click join: no download, no Zoom account, no meeting number typed by hand
  • Signature minted server-side per appointment and scoped to a single meeting
  • Attendee role by default; the same page joins your staff as host
  • Pre-join state shows the booking until the meeting is live, so the page is never empty
  • Leave returns the visitor to your page, not to a Zoom-hosted screen

If you'd rather not build this, Zoom Scheduler  handles booking and sends standard join links.

Follow along as we walk through the architecture.


Architecture

Component View mounts the meeting into a DOM node your page owns. That is the whole difference from Client View, which replaces your page with the full Zoom experience. Component View is the right choice when the surrounding page still matters during the call: the booking details, the intake form, the order being discussed.

One rule shapes everything else. The Meeting SDK Secret never reaches the browser. The browser asks your backend for a signature, and the backend returns a short-lived JWT naming exactly one meeting and one role. A signature in client-side code is a signature anyone can mint.

Components

ComponentResponsibilityOur stack (yours may differ)
Appointment pageRender your own layout and mount the meeting into itReact + Vite using @zoom/meetingsdk/embedded
Signature endpointAuthorize the visitor, then sign a scoped JWTExpress + jsonwebtoken
Booking sourceDecide which meeting this visitor may join, and as which roleIn-memory map in the sample; your database
graph LR
    V[Visitor] -->|1. open appointment page| P[Your Page]
    P -->|2. request signature| B[Backend]
    B -->|3. authorize booking| D[(Bookings)]
    B -->|4. signed JWT| P
    P -->|5. join| Z[Zoom Meeting]
    Z -->|6. media into your DOM node| P

The visitor opens a page that already knows which appointment it is showing. The page asks the backend for a signature, identifying the booking rather than the meeting. The backend decides whether this visitor may join that booking, then signs a JWT for the meeting behind it. The page calls init() with the element to render into, then join() with the signature.

Our stack vs. your options: We use Node/Express and React, but the backend only needs to sign an HS256 JWT, which every stack can do. The frontend only needs a DOM node and the SDK; Vue, Svelte, or plain JavaScript work the same way. The canonical meetingsdk-web-sample  ships React, Webpack, and CDN variants.

How the signature works

The payload shape is fixed by Zoom. Your backend fills it in and signs with the SDK Secret:

ClaimValue
appKey, sdkKeyYour Meeting SDK Key, in both fields
mnThe meeting number
role0 for attendee, 1 for host
iat, expIssued-at and expiry, in seconds
tokenExpSame value as exp

Expiry must sit between 1800 and 172800 seconds from issue, so 30 minutes at the shortest and 48 hours at the longest. Sign shorter rather than longer: a signature is a bearer credential for one meeting, and an appointment page can always ask for a fresh one.

Agent integration map

If you're grafting this into an existing codebase, check what you already have before adding anything:

Required capabilityReuse when presentAdd when missing
HTTP serverExisting API routesExpress route for the signature
Session or authExisting login and sessionWhatever identifies the visitor for a booking
Booking lookupExisting appointments tableIn-memory map keyed by booking ID
JWT signingExisting token utilityjsonwebtoken, HS256
Secret storageExisting secrets manager.env for local development
Frontend mount pointExisting page layoutOne div you control

Implementation Guide

Three parts: minting the signature, mounting the meeting, and running the reference implementations.

This blueprint deliberately does not restate the Meeting SDK. Zoom maintains two canonical samples that already do that well, and both are linked throughout:

RepoWhat it demonstrates
meetingsdk-web-sample Component View, Client View, and CDN variants of the raw SDK integration
meetingsdk-auth-endpoint-sample The signature endpoint on its own, with request validation

What this blueprint adds is the part those samples deliberately leave out: turning a meeting-number input box into an appointment page, where the visitor never sees a meeting number and the backend decides what they may join. meetingsdk-appointment-sample-js  is the small companion app that shows only that.

Part 1: Minting the Signature

Never sign in the browser

The SDK Secret is the credential that lets anyone join any meeting on your account as host. It belongs on the server, and nowhere else. This is the whole of the signing code:

import jwt from 'jsonwebtoken';

function createSignature({ meetingNumber, role, sdkKey, sdkSecret, expiresInSeconds = 1800 }) {
  const iat = Math.floor(Date.now() / 1000);
  const exp = iat + expiresInSeconds;

  // Zoom rejects an expiry outside this window.
  if (expiresInSeconds < 1800 || expiresInSeconds > 172800) {
    throw new Error('expiresInSeconds must be between 1800 and 172800');
  }

  return jwt.sign(
    {
      appKey: sdkKey,
      sdkKey,
      mn: String(meetingNumber),
      role,
      iat,
      exp,
      tokenExp: exp,
    },
    sdkSecret,
    { algorithm: 'HS256' },
  );
}

Other languages: Any HS256 JWT library works. In Python use PyJWT; in Go use golang-jwt. The canonical endpoint sample uses jsrsasign; jsonwebtoken produces the same token with one less dependency.

Signature endpoint

Input: A booking identifier from the page, plus whatever identifies the visitor (session cookie, signed link token, logged-in user)

Output: { signature, sdkKey, meetingNumber } for exactly one meeting

Invariants:

  • Take a booking ID from the client, never a meeting number. A client that can name its own meeting number can join any meeting on your account.
  • Authorize before signing. Resolve the booking, confirm this visitor is party to it, and only then look up the meeting number.
  • Default role to 0. Grant 1 only to the staff member the booking says is the host.
  • Return the SDK Key alongside the signature. It is public; the Secret is not.
  • Never log the signature or the Secret.
  • Reject a request for a booking whose appointment window has passed.

See the signature route  in the companion sample, and meetingsdk-auth-endpoint-sample  for the standalone version with full request validation.

Scoping a signature to a booking

Input: Booking record with meetingNumber, passcode, startsAt, hostUserId, attendeeName

Output: A decision: which meeting, which role, how long the signature lives

Invariants:

  • Signature lifetime tracks the appointment, not a fixed constant. An appointment at 3pm does not need a token minted at 9am.
  • The passcode travels with the join call, not inside the signature. It is a separate parameter.
  • One booking, one meeting. Reusing a single standing meeting across bookings means any past attendee can rejoin any future appointment.
  • Treat the booking lookup as the authorization boundary. Everything downstream trusts it.

Part 2: Mounting the Meeting

Give the SDK an element to render into

Component View renders into whatever node you hand it. Your layout decides where that node sits and how large it is:

import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded';

const client = ZoomMtgEmbedded.createClient();

await client.init({
  zoomAppRoot: document.getElementById('meeting-root'),
  language: 'en-US',
  patchJsMedia: true,
});

await client.join({
  signature,
  meetingNumber,
  userName,
  password: passcode,
});

zoomAppRoot is the only part that makes this Component View rather than Client View. In React, hold the node in a ref and call init() after the element has mounted, not during render.

Pin React to 18.2.0. @zoom/meetingsdk 6.2.0 declares an exact peer dependency, not a range, so npm install fails outright against React 19. The canonical meetingsdk-web-sample/Components  pins the same version. Reaching for --legacy-peer-deps to get around it means shipping the SDK against a React it was never tested with.

Budget for the bundle. A production build of the page in the companion sample is 3.25 MB, or 976 KB gzipped, almost all of it the SDK. That is fine behind a CDN with caching and slow on a first cold load, so it is worth knowing before someone reports the page as broken.

Init and join

Input: A mounted DOM node, and { signature, sdkKey, meetingNumber, passcode, userName } from your backend

Output: A joined meeting rendering inside your layout

Invariants:

  • Call init() once per page load. Calling it twice on the same root leaves two clients fighting over the node.
  • init() must resolve before join(). Both return promises; await them in order.
  • The element must exist and have a non-zero size before init(). A hidden or zero-height container renders nothing and reports no error.
  • Pass the passcode as password on join(), not in the signature.
  • Handle join() rejection as a user-facing state. An expired signature and a meeting that has not started fail differently and read identically if you only log them.
  • Tear down on unmount, so a client-side route change does not leave a live meeting attached to a removed node.

Other frameworks: Vue, Svelte, and plain JavaScript work identically; the SDK only needs an element. The canonical meetingsdk-web-sample/Components  is React with TypeScript and Vite.

Pre-join and leave states

Input: The booking record and the SDK client lifecycle

Output: A page that is useful before, during, and after the meeting

Invariants:

  • Render the booking before requesting a signature. A page that is blank until the SDK loads looks broken.
  • Request the signature on the visitor's click, not on page load. A signature minted on load is expiring while the visitor reads the page.
  • On leave, return to your own page. Component View leaves your layout intact, so there is no leaveUrl redirect to manage as there is in Client View.
  • Keep the meeting element mounted for the whole session. Unmounting mid-call drops the meeting.

Part 3: Running the Reference Implementations

The companion sample

meetingsdk-appointment-sample-js  is deliberately small: a signature endpoint, a fake bookings table, and one page that embeds the meeting. It exists to show the appointment framing, not to re-teach the SDK.

PlatformWhat you get
Deploy to Render Backend and page
Deploy to Railway Backend and page

Deploy anywhere: the repo includes a platform-agnostic Dockerfile .

Local setup

RequirementPurpose
Node.js 20+ Runtime
Zoom account Create the Meeting SDK app
A scheduled meetingSomething to join
Step-by-step local setup
  1. Create a Meeting SDK app at marketplace.zoom.us : Develop > Build App > Meeting SDK. Copy the SDK Key and SDK Secret.

  2. Clone and configure:

    git clone https://github.com/zoom/meetingsdk-appointment-sample-js.git
    cd meetingsdk-appointment-sample-js
    npm install
    cp .env.example .env
    
  3. Fill .env:

    ZOOM_MEETING_SDK_KEY=your_sdk_key
    ZOOM_MEETING_SDK_SECRET=your_sdk_secret
    
  4. Schedule a meeting in your Zoom client and put its number and passcode into bookings.js, which stands in for your database.

  5. Start:

    npm run dev
    
  6. Open the appointment link the console prints, and click Join.

When to use the canonical samples instead

Reach for meetingsdk-web-sample  when you want to compare Component View against Client View, or need the CDN build rather than npm. Reach for meetingsdk-auth-endpoint-sample  when you only need the signature service and already have a frontend.


App Manifest

The manifest.json in this directory records the Meeting SDK app configuration this blueprint assumes. A Meeting SDK app is simpler than a Zoom App: it has no OAuth scopes and no event subscriptions, because the credential it issues is used to sign meeting signatures rather than to call APIs on a user's behalf.

Credentials

ValueWhere it livesNotes
SDK KeyServer and browserPublic. Returned to the page alongside the signature.
SDK SecretServer onlySigns the JWT. Never ship it to the browser or commit it.

App type

In the Marketplace, choose Develop > Build App > Meeting SDK. This is a different app type from the General App used for Zoom Apps and OAuth integrations. A Meeting SDK app is what issues an SDK Key and Secret.

Domains

Add every origin that will embed the meeting to your app's domain allow list, including your local tunnel or localhost port during development. A missing origin fails at join() rather than at init(), which makes it look like a signature problem.


Production Considerations
AreaDevelopmentProduction
SDK Secret.env fileSecrets manager
BookingsIn-memory mapYour database, with the visitor authorization check
Signature lifetime30 minutesScoped to the appointment window
Signature endpointOpenBehind whatever authenticates the visitor
ServingVite dev serverStatic build behind your CDN

Authorization is the whole security model. The signature endpoint is the only thing standing between a visitor and every meeting on your account. If it accepts a meeting number from the client, or signs without checking who is asking, the embed is an open door. Everything else in this blueprint is presentation.

Cross-origin isolation. Some Meeting SDK features need Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers set on the page. Check the Component View documentation  for the current list before assuming a feature is broken.

Zoom for Government. The SDK targets a web endpoint that differs on government accounts. Configure it rather than accepting the commercial default, and confirm the current parameter in the Meeting SDK documentation .

Browser support and bundle size. Component View is WebAssembly. It works in current browsers, but the SDK dominates your bundle: 3.25 MB, 976 KB gzipped, in the companion sample. Serve it from a CDN, let it cache, and consider loading the SDK only on the appointment route rather than in your main bundle.


Acceptance Criteria

Use this checklist to verify the implementation:

  • The SDK Secret appears in no client bundle, no log line, and no API response
  • The signature endpoint accepts a booking identifier, never a meeting number
  • A request for a booking the visitor is not party to is rejected before any signing happens
  • role defaults to 0; 1 is granted only to the booking's host
  • Signature expiry is between 1800 and 172800 seconds and is scoped to the appointment
  • The signature is requested on the visitor's click, not on page load
  • init() resolves before join() is called
  • init() is called exactly once per page load
  • The mount element exists and has non-zero size before init()
  • The passcode is passed as password on join(), not embedded in the signature
  • An expired signature and a not-yet-started meeting produce different user-facing messages
  • Leaving the meeting returns the visitor to your page with the layout intact
  • Unmounting the page tears the client down rather than orphaning it
  • Every embedding origin is on the app's domain allow list
  • React is pinned to 18.2.0 rather than installed with --legacy-peer-deps