YC

Automating virtual arbitration hearings: Zoom OAuth, webhooks, and HMAC security

From one-click legal hearing scheduling to automated attendee recording distribution.

System: Legal Meeting Automator
Category: Event-Driven · Webhooks & APIs
Reading Time: 6 min read
Worked through: Zoom OAuth · HMAC SHA-256 Webhooks · FastAPI Background Tasks · SendGrid · Docker
01. WHY I BUILT IT

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.

02. PRACTICAL IMPACT

What It Solves in Practice

PRACTICAL PROBLEM

Generates compliant Zoom hearings in one click from the dispute portal, listens for recording completion, and automatically emails access links to verified attendees.

BENEFICIARIES

Arbitrators, corporate legal counsels, and hearing coordinators.

BEFORE THIS SYSTEM

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.

03. IMPLEMENTATION

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.

Key Decisions:
  • 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.
File: webhook_security.py
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")
04. CHALLENGES & SOLUTIONS

What Failed in Practice & How It Was Addressed

WHAT WENT WRONG
  • 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.
HOW I SOLVED IT
  • 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.
05. REFLECTION & LESSONS

Takeaways & Future Iterations

What I Learned:
  • 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.
WHAT I WOULD CHANGE NOW:

I would push incoming webhook payloads into an SQS queue and process them with a worker pool, decoupling receipt from processing entirely.