A small Go HTTP service that receives Zoom recording webhooks and streams the files directly into a Google Drive folder.
Designed to run on Google Cloud Run. Memory usage stays constant regardless of file size because it streams the download from Zoom into the Drive upload without buffering the whole file.
- Listens for Zoom webhook POSTs at
/webhook - Verifies the
x-zm-signatureheader on every event (HMAC-SHA256 with the webhook secret, 5-minute replay window) - Handles the
endpoint.url_validationhandshake - On
recording.completedandrecording.transcript_completed:- Enqueues a Cloud Tasks task for the event and ACKs Zoom immediately
- The queue (
max-concurrent-dispatches=1) serializes execution and provides durable retry
- Cloud Tasks dispatches each task to
/process-event(OIDC-authenticated) which does the actual Zoom → Drive work synchronously:- Creates a Drive folder structure:
<root>/<host_username>/<YYYY-MM-DDThh-mm>-<topic>/raw/wherehost_usernameis the lowercased local part of the meeting host's email (e.g.,skapadiafromskapadia@chariotsolutions.com) - Streams each recording file (MP4, M4A, timeline.json, transcript VTT)
from Zoom into Drive using the per-event
download_tokenfor auth - Writes a
meeting-metadata.jsonfile (only on the initialrecording.completedevent to avoid overwriting)
- Creates a Drive folder structure:
The two-endpoint design exists because Cloud Run throttles CPU
between inbound requests and can reap idle instances — background
goroutines spawned from /webhook would get killed mid-upload. See
docs/design-decisions.md "Decision 6"
and issue #8
for the full story.
- Transcripts are not guaranteed to arrive. Zoom fires
recording.transcript_completedonly when transcript generation succeeds — short/silent/unsupported-language meetings may never produce one. In that case the other files land normally and the folder simply lacks a transcript.
| Variable | Description |
|---|---|
ZOOM_WEBHOOK_SECRET_TOKEN |
From your Zoom Marketplace app → Feature → Secret Token |
DRIVE_ROOT_FOLDER_ID |
Google Drive folder ID where recordings will land |
PROCESS_EVENT_URL |
Full URL of this service's /process-event endpoint (e.g. https://…run.app/process-event) |
CLOUD_TASKS_QUEUE |
Queue resource name, projects/.../locations/.../queues/… |
TASKS_INVOKER_SA |
Service account email Cloud Tasks impersonates for OIDC tokens |
PORT |
(Optional, defaults to 8080) |
BRIDGE_IN_PROCESS_FAKE_TASKS |
(Test only) Set to 1 for local-dev/synthetic runs — short-circuits Cloud Tasks and OIDC verification. Never set in production. |
See .env.example for a template.
# Copy the env template and fill in real values
cp .env.example .env
# Build and run
go build -o server .
./server
# Or run directly
go run .gcloud run deploy zoom-recording-bridge \
--source . \
--region us-east1 \
--allow-unauthenticated \
--service-account <YOUR_SERVICE_ACCOUNT> \
--set-env-vars DRIVE_ROOT_FOLDER_ID=... \
--update-secrets ZOOM_WEBHOOK_SECRET_TOKEN=zoom-webhook-secret:latestAfter deployment, copy the service URL and paste it (with /webhook appended)
into your Zoom app's Event Subscription endpoint.
The service uses Application Default Credentials to authenticate with the
Drive API. On Cloud Run, attach a service account that has Drive access to the
target folder. Locally, run gcloud auth application-default login first.
| Path | Method | Purpose |
|---|---|---|
/ |
GET | Liveness check (returns plain text) |
/webhook |
POST | Zoom webhook receiver |
docs/deployment.md— full deployment guide (Cloud Run setup, Secret Manager, Zoom app config, end-to-end testing)docs/design-decisions.md— why the code looks the way it does: design tradeoffs, alternatives considered, and bugs caught during developmentdocs/synthetic-test-driver.md— the synthetic webhook test driver's design rationale
Licensed under the Apache License, Version 2.0. See LICENSE
for the full text and NOTICE for attribution.