# Quick Start - Hello World Zoom App Complete working Zoom App with Express backend and SDK frontend. ## Prerequisites - Node.js 18+ - ngrok account (free tier works) - Zoom Marketplace app (type: "Zoom App") ## Project Setup ### package.json ```json { "name": "zoom-app-hello-world", "version": "1.0.0", "scripts": { "start": "node server.js", "dev": "nodemon server.js" }, "dependencies": { "express": "^4.18.0", "cookie-session": "^2.0.0", "axios": "^1.6.0", "dotenv": "^16.0.0" } } ``` ### .env ```ini ZOOM_APP_CLIENT_ID=your_client_id ZOOM_APP_CLIENT_SECRET=your_client_secret ZOOM_APP_REDIRECT_URI=https://xxxxx.ngrok.io/auth SESSION_SECRET=generate_a_random_string_here ``` ### server.js ```javascript require('dotenv').config(); const express = require('express'); const crypto = require('crypto'); const cookieSession = require('cookie-session'); const axios = require('axios'); const app = express(); app.use(express.json()); app.use(express.static('public')); // Cookie session - SameSite=None required for Zoom embedded browser app.use(cookieSession({ name: 'session', keys: [process.env.SESSION_SECRET], maxAge: 24 * 60 * 60 * 1000, sameSite: 'none', secure: true })); // OWASP security headers (required for Marketplace approval) app.use((req, res, next) => { res.setHeader('Strict-Transport-Security', 'max-age=31536000'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Security-Policy', "frame-ancestors 'self' zoom.us *.zoom.us"); res.setHeader('Referrer-Policy', 'same-origin'); next(); }); // Step 1: Install - redirect to Zoom OAuth app.get('/install', (req, res) => { const verifier = crypto.randomBytes(32).toString('hex'); const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); const state = crypto.randomBytes(16).toString('hex'); req.session.codeVerifier = verifier; req.session.state = state; const url = `https://zoom.us/oauth/authorize?` + `client_id=${process.env.ZOOM_APP_CLIENT_ID}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(process.env.ZOOM_APP_REDIRECT_URI)}` + `&code_challenge=${challenge}` + `&code_challenge_method=S256` + `&state=${state}`; res.redirect(url); }); // Step 2: OAuth callback - exchange code for tokens app.get('/auth', async (req, res) => { const { code, state } = req.query; if (state !== req.session.state) { return res.status(403).send('Invalid state'); } try { // Exchange authorization code for tokens const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { params: { grant_type: 'authorization_code', code, redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, code_verifier: req.session.codeVerifier }, headers: { 'Authorization': 'Basic ' + Buffer.from( `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` ).toString('base64') } }); req.session.tokens = tokenResponse.data; // Get deeplink to open app in Zoom client const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', { action: '' }, { headers: { 'Authorization': `Bearer ${tokenResponse.data.access_token}` } } ); res.redirect(deeplink.data.deeplink); } catch (error) { console.error('OAuth error:', error.response?.data || error.message); res.status(500).send('Authorization failed'); } }); // API endpoint for frontend app.get('/api/user', (req, res) => { if (!req.session.tokens) { return res.status(401).json({ error: 'Not authenticated' }); } res.json({ authenticated: true }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); ``` ### public/index.html ```html