Zoom API
Server-to-Server OAuth, the endpoints and scopes, and the four gotchas that will bite.
Zoom API
One Server-to-Server OAuth app, created by the account owner. No user
consent flows, no refresh tokens, whole-account reach with :admin scopes. The
token is grant_type=account_credentials with a 1-hour TTL, and no marketplace
review is needed for a private app.
Five employees, one internal script, an admin who can flip account settings. User OAuth buys nothing and costs five authorization flows.
Endpoints and scopes
| Purpose | Call | Scope |
|---|---|---|
| Enumerate | GET /meetings/meeting_summaries |
meeting:read:list_summaries:admin |
| Fetch | GET /meetings/<meeting-uuid>/transcript |
cloud_recording:read:meeting_transcript:admin |
| Summary body | GET /meetings/<id>/meeting_summary |
meeting:read:summary:admin |
| Fallback enum | GET /report/users/<user-id>/meetings |
report:read:user:admin |
| Occurrences | GET /past_meetings/<id>/instances |
meeting:read:list_past_instances [UNVERIFIED] |
| Participants | GET /past_meetings/<meeting-uuid>/participants |
report: family [UNVERIFIED] |
The enumerators take from= and to=. Read the exact granular strings off the
app’s scope picker — the two rows marked above were not read from a primary
source.
Step one of the build: create the app as the account owner and screenshot
the scope list. A 2026 dev forum thread reported
meeting:read:list_summaries:admin could not be added to an S2S app; it
resolved as a role permission on the creator’s user, not an S2S limit
[UNVERIFIED]. That scope is the enumerator for the whole design. Fallback is
in Pipeline.
Poll, don’t listen
meeting.aic_transcript_completed is the correct event and we still do not use
it.
WEBHOOK POLL, 7-day overlapping window
───────────────────────── ─────────────────────────────
3 failed deliveries and Zoom Mac asleep, laptop shut,
STOPS SENDING THE EVENT script crashed — the next
until a human re-validates run catches all of it,
in the console. silently.
Silent, unbounded data loss. Self-healing.
Needs a public endpoint, HMAC
verification, a url_validation
handshake.
Retries go +5 min, +20 min, +60 min, then permanent mute. For five people with no on-call rota, “heals itself silently” beats “fails silently”.
Latency cost: next morning instead of an hour after the call. Zoom publishes no processing SLA, and third-party observation puts it near 2× meeting duration, occasionally 24 hours. The difference is mostly imaginary.
Gotchas
- UUID is the primary key, not meeting ID. A recurring meeting reuses one ID across every occurrence. The UUID is per-occurrence.
- Double URL-encode UUIDs containing
/. They are base64, so/,+and=all appear. Single-encoding gives a 500 or a 3001. /transcriptwants the past-meeting instance UUID, not the scheduled meeting ID. Wrong one gives error 3322, “This meeting transcript does not exist”. Zoom’s docs are wrong on this and a staffer said so.- Rate limits are a non-issue at five meetings a day. Skip the backoff engineering. Just do not loop.