TL;DR: Zoom's marketplace hid the Server-to-Server OAuth option in 2024. The old compound scopes don't work anymore. And
/users/meflat-out doesn't exist in S2S. Here's the fast path so you don't lose two hours like I did.
the setup
We're building a board view inside an internal KOL webinar CRM — my main ecommerce client's tool for managing KOL (key opinion leader) webinar relationships.
Every webinar gets a card. I wanted live registrant counts right on those cards.
Not "open Zoom and manually check." Just… the number. Right there. Every time you look at the board.
That meant wiring up Zoom's Server-to-Server OAuth — machine credentials, no user login, server-side API calls. Should've been a 20-minute job.
It was not a 20-minute job.
what broke first: the hidden app type
I opened marketplace.zoom.us to create an S2S app.
The chooser showed: General app and Webhook only.
That's it.
I spent way too long clicking around thinking I was missing something obvious. I wasn't missing something obvious — Zoom moved it. In 2024 they rolled out a "Unified Build Flow" and Server-to-Server OAuth is now gated behind a role permission in account settings. It doesn't appear in the default chooser at all.
(This is the thing that ate roughly two hours. Writing it down so it doesn't eat two of yours.)
what broke second: two dead ends
Once I unlocked S2S and got the app created, I hit two more walls back to back.
Wall one — the Dashboard endpoint. I tried /v2/metrics/webinars to pull webinar stats. The response was a polite error: "This API is only available for ZMP and Business or higher accounts that have enabled the Dashboard feature." My ecommerce client is on Pro. Pro doesn't include Dashboard. That endpoint is permanently off-limits.
Wall two — compound scopes are dead. I added the old scopes — webinar:read:admin, report:read:admin — and they technically accepted but weren't granular enough to actually authorize the calls I needed. Zoom moved to hyper-granular scope names in the same 2024 refresh. The compound scopes don't grant the granular sub-scopes. They're dead weight.
the fix that actually worked
Three things, in order:
-
Use granular scopes only. The ones that work for registrant counts:
webinar:read:list_webinars:adminandwebinar:read:list_registrants:admin. Adduser:read:list_users:adminif you need to enumerate hosts. Skipdashboard:read:list_webinars:adminentirely unless you're on Business+. -
Pin the host user ID.
/users/medoesn't exist in S2S. There's no user context — it's a machine credential.GET /users/mereturns400 code 4711. I pinned my client's host user ID inZOOM_WEBINAR_HOST_USER_IDand called/users/{hostId}/webinars?page_size=300. One call, all 28 webinars returned. -
page_size=1+total_recordsfor counts. Hit/webinars/{id}/registrants?page_size=1and readtotal_recordsfrom the response envelope. You don't need to paginate through actual registrant records — the count is right there.
Token minting is one curl:
POST https://zoom.us/oauth/token?grant_type=account_credentials&account_id=<your_account_id>
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
Token lives 1 hour. Cache it in memory for ~50 minutes and re-mint. No refresh token — just mint fresh when it expires.
why this matters to me
The cards now show 👥 1,948 registered for my client's top webinar.
That number — right there on the card — is WILD to see for the first time. 1,948 people signed up for one webinar. The whole team can feel the scale of what they're running now, without opening a single Zoom tab.
The auth setup shouldn't have taken two hours. Now it's documented. The next time I wire Zoom to something I've built, it'll take twenty minutes — and so will yours.