Node.js sample
Zoom API and SDK reference
Runnable Express examples for Zoom webhooks, Server-to-Server OAuth, Client Credentials OAuth, OAuth redirects, Meeting SDK signatures, token refresh, REST API calls, and token verification.
Live demo
Code samples
Webhook handler
// Define a function to handle webhook requests
function handleWebhookRequest(req, res, secretToken, path) {
if (req.method === 'POST') {
// Check if the event type is "endpoint.url_validation"
if (req.body.event === 'endpoint.url_validation') {
const hashForValidate = crypto.createHmac('sha256', secretToken)
.update(req.body.payload.plainToken)
.digest('hex');
res.status(200).json({
"plainToken": req.body.payload.plainToken,
"encryptedToken": hashForValidate
});
} else {
res.status(200).send();
}
}else if (req.method === 'GET') {
} else {
// Handle unsupported HTTP methods
res.status(405).send("Method Not Allowed");
}
}
app.post('/webhook/', (req, res) => {handleWebhookRequest(req,res, process.env.ZOOM_WEBHOOK_SECRET_TOKEN,"webhook");});
app.get('/webhook/', (req, res) => {handleWebhookRequest(req,res, process.env.ZOOM_WEBHOOK_SECRET_TOKEN,"webhook");});
Server-to-Server OAuth sample
app.get('/s2soauth', (req, res) => {
res.status(200).json({
message: 'Live S2S OAuth token requests are disabled on this public demo.',
liveTokenRequestDisabled: true,
grantType: 'account_credentials',
tokenUrl: 'https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ACCOUNT_ID}',
requestShape: {
method: 'POST',
headers: {
Authorization: 'Basic base64({CLIENT_ID}:{CLIENT_SECRET})',
'Content-Type': 'application/x-www-form-urlencoded',
},
},
expectedResponseFields: [
'access_token',
'token_type',
'expires_in',
'scope',
'api_url',
],
});
});
Client Credentials OAuth sample
app.get(['/clientcredential', '/clientcredentials'], (req, res) => {
const requestedClientId = (req.query.clientid || req.query.clientId || '').trim();
res.status(200).json({
message: 'Live client credentials token requests are disabled on this public demo.',
liveTokenRequestDisabled: true,
reason: 'This GET endpoint intentionally does not load a client ID, client secret, or return an access token.',
requestedClientId: requestedClientId || undefined,
grantType: 'client_credentials',
tokenUrl: 'https://zoom.us/oauth/token?grant_type=client_credentials',
requestShape: {
method: 'POST',
headers: {
Authorization: 'Basic base64({CLIENT_ID}:{CLIENT_SECRET})',
'Content-Type': 'application/x-www-form-urlencoded',
},
},
zoomResponseSummary: 'Zoom returns bearer token metadata from a private server-side POST. This public GET route does not proxy that response.',
});
});
OAuth redirect sample
app.get('/redirecturlforoauth', (req, res) => {
const type = req.query.type || '';
const requestedClientId = (req.query.clientid || req.query.clientId || '').trim();
const hasCode = Boolean(req.query.code);
res.status(200).json({
message: 'Live OAuth authorization-code token exchange is disabled on this public demo.',
liveTokenExchangeDisabled: true,
reason: 'This GET endpoint intentionally does not load OAuth client credentials, read cached token files, exchange ?code= values, or return an access token.',
codeReceived: hasCode,
requestedType: type || undefined,
requestedClientId: requestedClientId || undefined,
grantType: 'authorization_code',
authorizeUrl: 'https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}',
tokenExchangeShape: {
method: 'POST',
url: 'https://zoom.us/oauth/token',
headers: {
Authorization: 'Basic base64({CLIENT_ID}:{CLIENT_SECRET})',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
grant_type: 'authorization_code',
code: '{AUTHORIZATION_CODE}',
redirect_uri: 'https://nodejs.asdc.cc/redirecturlforoauth',
},
},
zoomResponseSummary: 'Zoom returns bearer and refresh token metadata from a private server-side POST. This public GET route does not proxy that response.',
});
});
Meeting SDK signature
app.post('/meeting/', (req, res) => {
const now = Math.round((new Date().getTime()) / 1000)
//make it valid 5 minute ago
const iat =now - 5 * 60;
//make it expire 30 minutes later
const exp = now + 30 * 60;
const oHeader = { alg: 'HS256', typ: 'JWT' }
const oPayload = {
appKey: process.env.ZOOM_SDK_KEY,
mn: req.body.meetingNumber,
role: req.body.role,
iat: iat,
exp: exp,
tokenExp: exp
}
const sHeader = JSON.stringify(oHeader)
const sPayload = JSON.stringify(oPayload)
const signature = KJUR.jws.JWS.sign('HS256', sHeader, sPayload, process.env.ZOOM_SDK_SECRET)
res.json({
signature: signature,
appKey: process.env.ZOOM_SDK_KEY
})
})
OAuth refresh token
app.get('/oauthrefreshtoken', async (req, res) => {
const refreshToken = req.query.code;
const clientId = process.env.ZOOM_OAUTH_USERLEVEL_CLIENT_ID;
const clientSecret = process.env.ZOOM_OAUTH_USERLEVEL_CLIENT_SECRET;
if (!refreshToken) {
return res.status(400).json({ error: 'Missing ?code=refresh_token parameter' });
}
try {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
new URLSearchParams({
refresh_token: refreshToken,
grant_type: 'refresh_token',
}).toString(),
{
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({ error: error.response?.data || error.message });
}
});
REST API call
// Function to make a REST API request using the bearer token
async function makeApiRequestWithToken(bearerToken,meetingNumber) {
try {
const apiUrl = `https://api.zoom.us/v2/meetings/${meetingNumber}/jointoken/local_recording`;
// Define your API request parameters
const apiRequestData = {
method: 'GET', // Change to the HTTP method you need
url: apiUrl, // Replace with your API endpoint URL
headers: {
'Authorization': `Bearer ${bearerToken}`,
// Add other headers as needed
},
};
// Send the API request with the bearer token
const response = await axios(apiRequestData);
const recordingToken = response.data.token;
// Return the response data
return recordingToken ;
} catch (error) {
console.error('Error making API request:', error.message);
return error.message ; // Optionally rethrow the error
}
}
Environment variables
ZOOM_VIDEO_SDK_KEY="xxxxxxx"
ZOOM_VIDEO_SDK_SECRET="xxxxxxx"
#ZOOM_SDK_KEY="xxxxxxx"
#ZOOM_SDK_SECRET="xxxxxxx"
ZOOM_WEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_MSDKWEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_SDK_KEY="xxxxxxx"
ZOOM_SDK_SECRET="xxxxxxx"
ZOOM_CLIENT_ID_NEW="xxxxxxx"
ZOOM_CLIENT_SECRET_NEW="xxxxxxx"
ZOOM_CLIENT_CREDENTIALS_CLIENT_ID="xxxxxxx"
ZOOM_CLIENT_CREDENTIALS_CLIENT_SECRET="xxxxxxx"
ZOOM_VSDK_WEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_S2SOAUTH_WEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_S2S_CLIENT_ID="xxxxxxx"
ZOOM_S2S_CLIENT_SECRET="xxxxxxx"
ZOOM_S2S_ACCOUNTID="xxxxxxx"
ZOOM_OAUTH_ACCOUNTLEVEL_WEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_OAUTH_ACCOUNTLEVEL_CLIENT_ID="xxxxxxx"
ZOOM_OAUTH_ACCOUNTLEVEL_CLIENT_SECRET="xxxxxxx"
ZOOM_OAUTH_USERLEVEL_WEBHOOK_SECRET_TOKEN="xxxxxxx"
ZOOM_OAUTH_USERLEVEL_CLIENT_ID="xxxxxxx"
ZOOM_OAUTH_USERLEVEL_CLIENT_SECRET="xxxxxxx"