A fully auto-generated, dependency-free, and 100% typed Zoom API client for Node.js. Generated from Zoom's official OpenAPI specification.
Maintained by Nektar AI and used in production.
npm install @nektarai/zoom-api-client- Zero Dependencies: No runtime dependencies
- Fully Typed: Complete TypeScript type definitions generated from OpenAPI spec
- Comprehensive Coverage: 180+ endpoints across meetings, webinars, users, reports, recordings, and more
- Auto-Generated: Regenerate anytime from the latest Zoom OpenAPI spec
- Tested: Full test coverage with Jest
This is the core provider of all authentication and requests for Zoom.
It is recommended to create only one ZoomClient instance for all Oauth (non-S2SO) purposes.
import {ZoomClient} from '@nektarai/zoom-api-client';
const zoomClient = new ZoomClient({
clientId: process.env.ZOOM_CLIENT_ID,
clientSecret: process.env.ZOOM_CLIENT_SECRET,
redirectUri: process.env.ZOOM_REDIRECT_URI,
verificationKey: process.env.ZOOM_VERIFICATION_KEY, // optional
});To use the Zoom's Oauth functionality
import {ZoomS2SO} from '@nektarai/zoom-api-client';
const zoomOauth = new ZoomOauth(zoomClient);
expressRouter.get('/zoom/oauth', (req, res) => {
const state = {userId: req.params.userId};
res.redirect(zoomOauth.getAuthorizationUrl(state));
});To use Zoom's Server-to-server oauth functionality
import {ZoomClient, ZoomS2SO} from '@nektarai/zoom-api-client';
const zoomClient = new ZoomClient({
// S2SO credentials
})
const zoomS2so = new ZoomS2SO(zoomClient);The API client provides access to all Zoom API endpoints through an intuitive, fluent interface.
import {ZoomApi} from '@nektarai/zoom-api-client';
const zoomApi = new ZoomApi({
client: zoomClient,
tokens: await zoomS2so.requestTokens(),
});
// List meetings for a user
const meetings = await zoomApi.user('me').listMeetings({ type: 'scheduled' });
// Create a meeting
const newMeeting = await zoomApi.user('me').createMeeting({
topic: 'Team Standup',
type: 2, // Scheduled meeting
start_time: '2026-02-15T10:00:00Z',
});// Get meeting recordings
const recordings = await zoomApi.meeting('meeting-id').listRecordings();
// Get past meeting details and participants
const details = await zoomApi.pastMeeting('meeting-uuid').getPastMeeting();
const participants = await zoomApi.pastMeeting('meeting-uuid').listParticipants();// Get daily usage report
const report = await zoomApi.report().getDaily({ year: 2024, month: 1 });// List Zoom Rooms devices
const devices = await zoomApi.devices().list();// Webinar operations
const webinar = await zoomApi.webinar('webinar-id').getWebinar();
const registrants = await zoomApi.webinar('webinar-id').listRegistrants();Any non-ok response throws a ZoomError carrying the HTTP status, Zoom's application error code
from the response body, and rate-limit state parsed from the response headers.
import { ZoomError } from '@nektarai/zoom-api-client';
try {
await zoomApi.report().listMeetings(userId, { from, to });
} catch (err) {
if (!(err instanceof ZoomError)) throw err;
err.code; // Zoom's body error code, e.g. 124 (invalid access token)
err.statusCode; // HTTP status, e.g. 429
err.retryAfter; // `Retry-After` in whole seconds, or undefined
err.rateLimit; // { type, category, limit, remaining, reset }, or undefined
}OAuth failures return { error, reason } rather than message, so those arrive joined as
err.message === 'invalid_grant: Invalid Token!' — the code stays matchable by substring while the
readable half remains visible.
rateLimit.type is what distinguishes a per-second trip from a spent daily quota, which need very
different retry strategies:
const DEFAULT_BACKOFF_SECONDS = 60;
if (err.statusCode === 429) {
if (err.rateLimit?.type === 'Daily-limit') {
// Quota is gone for the day. Reschedule past err.rateLimit.reset
// instead of retrying — every retry before then also fails.
reschedule(err.rateLimit.reset);
} else {
// QPS. `retryAfter` is optional, so always supply a fallback.
const wait = err.retryAfter ?? DEFAULT_BACKOFF_SECONDS;
setTimeout(retry, wait * 1000);
}
}Note the ?? — retryAfter is number | undefined and Zoom does not always send Retry-After,
even on a 429. Using it unguarded gives undefined * 1000 → NaN, which setTimeout treats as
0 and retries instantly against an endpoint that just rejected you.
rateLimit is undefined when Zoom sent no X-RateLimit-* headers, so an absent value means "no
data", not "not rate limited". Fall back to statusCode and the message in that case.
Known limitation: rate-limit headers are read on the error path only. X-RateLimit-Remaining on
a successful response is discarded, because request() resolves with the parsed body and has
nowhere to surface it. So you can react to a 429 but cannot yet throttle to avoid one — track your
own call counts if you need that. Exposing the headers on success is additive and would not be a
breaking change.
This library is auto-generated from Zoom's OpenAPI specifications. Zoom publishes one spec per
product area; each is committed verbatim under specs/ and registered in SPEC_PATHS in
scripts/generate-api.ts. To regenerate:
npm run generateThis will:
- Parse each OpenAPI spec in
specs/ - Generate TypeScript types in
src/types.generated.ts - Generate API client methods in
src/zoomApi.generated.ts - Format and lint the generated code
Upgrading from 0.x? Version 1.0.0 replaced the hand-written convenience methods with a pure OpenAPI-generated API. See the migration guide for the old-to-new method mapping.
We welcome contributions!
- Clone the repo
- Install dependencies:
npm install - Make your changes:
- For API updates: refresh or add a spec in
specs/and runnpm run generate - For core functionality: Edit files in
src/ - Add tests in
test/
- For API updates: refresh or add a spec in
- Ensure tests pass:
npm test - Build:
npm run build - Submit a PR