---
title: Zoom API
description: "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](/be-pipeline-meeting-transcripts/pipeline).

## Poll, don't listen

`meeting.aic_transcript_completed` is the correct event and we still do not use
it.

```text
  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.
- **`/transcript` wants 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.
