Automating virtual arbitration hearings: Zoom OAuth, webhooks, and HMAC security
From one-click legal hearing scheduling to automated attendee recording distribution.
The Trigger & Underlying Problem
The Trigger: During high-volume virtual arbitration hearings, coordinating Zoom links and afterwards downloading and emailing recordings was consuming hours of staff time.
The Core Problem: Legal hearings have strict compliance rules: waiting rooms must be locked, host-only video permissions must be enforced, and cloud recordings must be securely distributed to all attendees within hours of adjournment.
Why Standard Solutions Were Insufficient: Standard calendar plugins do not enforce mandatory legal hearing compliance templates, nor do they listen to recording completion webhooks to distribute gated links automatically.
What It Solves in Practice
Generates compliant Zoom hearings in one click from the dispute portal, listens for recording completion, and automatically emails access links to verified attendees.
Arbitrators, corporate legal counsels, and hearing coordinators.
Staff manually generated Zoom links, set passwords, pasted links into emails, waited for Zoom emails days later, downloaded 2GB videos, and re-uploaded them to Google Drive.
Architecture & Key Engineering Decisions
A lightweight FastAPI webhook service containerized and deployed behind Nginx. It communicates with the Zoom REST API using Server-to-Server OAuth credentials.
- Used Zoom Server-to-Server OAuth with in-memory token caching to avoid storing credentials in files on disk.
- Verified HMAC SHA-256 signatures on every incoming Zoom webhook to reject spoofed payloads at the network edge.
- FastAPI background tasks handle email dispatch asynchronously so the webhook endpoint returns HTTP 200 within Zoom's 3-second acknowledgement window.
import hmac, hashlib
from fastapi import Request, HTTPException
WEBHOOK_SECRET = "zoom-webhook-secret-token"
async def verify_zoom_signature(request: Request):
timestamp = request.headers.get("x-zm-request-timestamp", "")
signature = request.headers.get("x-zm-signature", "")
body = await request.body()
message = f"v0:{timestamp}:{body.decode('utf-8')}"
expected = "v0=" + hmac.new(WEBHOOK_SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(status_code=401, detail="Invalid webhook signature")What Failed in Practice & How It Was Addressed
- Zoom deprecated JWT authentication midway through development, requiring a migration to Server-to-Server OAuth.
- Zoom retried webhook events repeatedly because our initial email dispatch ran synchronously, causing the response to exceed Zoom's 3-second timeout.
- Some recordings were split into multiple parts (audio-only vs gallery view), leading to duplicate emails.
- Built an OAuth token manager that automatically refreshes the bearer token 5 minutes before expiration.
- Moved all notification and token extraction work into FastAPI BackgroundTasks, allowing the HTTP endpoint to return an immediate HTTP 200 status in under 50ms.
- Filtered payload recording files by `file_type == "MP4"` and `recording_type == "shared_screen_with_speaker_view"`, sending one consolidated link bundle per hearing ID.
Takeaways & Future Iterations
- Webhook endpoints must be fast and defensive. Never perform heavy I/O before acknowledging an external webhook receipt.
- Always verify cryptographic signatures on third-party webhooks.
I would push incoming webhook payloads into an SQS queue and process them with a worker pool, decoupling receipt from processing entirely.