clients/zoom-app/ that you host on your own infrastructure; it sends meeting context to your MeetCost backend and renders what comes back, without ever receiving or holding pay data.
How the app works
- In a meeting
- Outside a meeting
When loaded inside the Zoom client, the app calls
zoomSdk.getMeetingContext() to confirm it is in a live meeting, then calls zoomSdk.getMeetingParticipants() every 60 seconds to collect participant email addresses. Each tick posts to /api/v1/estimate on your backend with the current elapsed minutes and the participant list. The running total updates on screen as the clock runs.If Zoom does not return participant emails — which depends on scopes and whether participants are in the same account — the panel says so rather than silently costing a smaller meeting than the one taking place.Prerequisites
- A deployed MeetCost backend reachable over HTTPS.
- A Zoom Server-to-Server OAuth app created in the Zoom Marketplace. This app type uses account-level credentials — no user authorization flow — which is what the backend adapter uses for directory sync and the Reports API.
- Hosting for the static files in
clients/zoom-app/(or serve them from the MeetCost service itself; see below).
Installation
1
Host the static files
Serve the
clients/zoom-app/ directory over HTTPS from your own infrastructure. If you serve them from the MeetCost service itself, the client automatically uses the page origin as the backend URL and no config.js is needed at all.Otherwise, copy the example config and set the backend address:2
Vendor the Zoom Apps SDK
The app loads the Zoom SDK from Loading a script from a third-party CDN at runtime means that party controls the page’s behaviour. Vendoring eliminates that dependency.
vendor/zoom-appssdk.min.js, not from a CDN. Download the SDK bundle and place it there:3
Create the Zoom Marketplace app
In marketplace.zoom.us, create a Zoom App with these settings:
- Home URL: your hosted
index.html - Domain allow list: your MeetCost hostname
- Scopes:
user:read,meeting:read(for the client-side panel), plususer:read:adminandreport:read:adminon the server-to-server credentials the backend uses - Features → APIs:
getMeetingContext,getUserContext,getMeetingParticipants,onParticipantChange
4
Set the server-to-server credentials
On your MeetCost backend, set these environment variables using the credentials from your Zoom Server-to-Server OAuth app:
When these variables are set,
api.zoom.us and zoom.us are automatically added to the backend’s egress allowlist.5
Configure the webhook
In the Zoom Marketplace app, under Event Subscriptions, subscribe to Meeting → Meeting has ended and set the notification endpoint to:Zoom will perform a URL validation handshake; the MeetCost backend answers it automatically.
Authentication
The Zoom app does not use Zoom’s identity for MeetCost authentication — Zoom app scopes do not include a general identity token suitable for server verification. Instead,config.js supplies a MEETCOST_GET_TOKEN function appropriate to how your organisation authenticates.
- SSO proxy (recommended)
- Pre-injected token
If the app is served behind an SSO proxy such as
oauth2-proxy, the proxy can expose the user’s identity token on the same origin. This is the default behaviour when MEETCOST_GET_TOKEN is not defined:Webhook: meeting.ended
When a meeting ends, Zoom sends ameeting.ended event to the configured endpoint. The MeetCost backend:
- Verifies the
x-zm-signatureheader — an HMAC-SHA256 signature overv0:<timestamp>:<raw body>usingMEETCOST_ZOOM_WEBHOOK_SECRET. - Uses the meeting ID from the body to fetch attendance from the Reports API with the backend’s own server-to-server credentials.
- Stores the resulting
MeetingRecordfor post-meeting summary queries.
The URL validation handshake Zoom sends when you first configure the subscription is handled automatically. You do not need to do anything special; just set
MEETCOST_ZOOM_WEBHOOK_SECRET before the endpoint receives the first request.Signature verification
Directory sync
The Zoom directory adapter authenticates via server-to-server OAuth (account credentials grant) againsthttps://zoom.us/oauth/token, then pages through GET https://api.zoom.us/v2/users with status=active and page_size=300. Each user’s dept field becomes their team_id for reports grouping.
The required admin scopes are user:read:admin and report:read:admin.
Attendance collection: reconnection handling
Zoom emits one Reports API row per join event. When a participant drops and rejoins, they appear twice. The Zoom adapter sums their connected seconds across all rows rather than counting them as two attendees:Local development
Serve the folder directly and open it in a browser — no Zoom client needed:zoomSdk is absent, the page automatically falls back to the manual estimator. Set window.MEETCOST_BACKEND_URL in config.js to your local backend and the estimator works end to end.
Security model
No pay data to Zoom
The app sends attendee email addresses and elapsed time to your backend. It never receives a salary, rate, or cost figure — the backend redacts before responding.
No default backend
There is no fallback URL. A client without a configured
MEETCOST_BACKEND_URL renders a setup message and contacts nothing. The build test test_no_egress.py enforces this.Vendored SDK
The Zoom Apps SDK is loaded from
vendor/, not a CDN. Third-party CDNs control the files they serve; vendoring removes that dependency from your production page.Forged webhook safety
Attendance comes from the Reports API using server credentials, not from the webhook body. HMAC verification prevents replay; a forged or replayed webhook has no pay data to inject.