Skip to content

Latest commit

 

History

History
88 lines (46 loc) · 4.61 KB

File metadata and controls

88 lines (46 loc) · 4.61 KB

API notes

Practical gotchas for Zoom and Attio, derived from this repo's code and docs/setup.md. Not a substitute for vendor docs.

Zoom

Webhook download_token placement

Zoom places download_token at the top level of the webhook JSON body for recording.completed, not only inside payload. The parser in src/zoom/webhooks.ts checks both locations. The queue processor can also fetch a fresh token via GET /meetings/{uuid}/recordings?include_fields=download_access_token.

3-second webhook deadline

The reference webhook handler (example/nextjs/app/api/webhooks/zoom/route.ts) only verifies, dedup-claims, and enqueues. Heavy work runs in processZoomAttioBridgeQueue.

Recurring meeting IDs vs UUIDs

A numeric meeting ID resolves to the latest instance of a recurring meeting. The bridge always uses the instance UUID for recordings, delete, and download (encodeZoomMeetingUuid in src/zoom/client.ts).

No invitee list endpoint

GET /meetings/{id}/invitees does not exist (Zoom error 2300). Pre-meeting filtering uses settings.meeting_invitees from the single-meeting GET when present; ingest-time policy uses GET /past_meetings/{uuid}/participants.

External participant emails withheld

Zoom withholds user_email for participants outside the host's Zoom account. The bridge uses Zoom's internal_user boolean as the primary externality signal, with INTERNAL_EMAIL_DOMAINS as fallback when an email is present.

Transcript lag

recording.completed can fire before the VTT is ready. Optional recording.transcript_completed webhook speeds pickup; otherwise the transcript retry queue polls Zoom for up to 24h (TRANSCRIPT_MAX_WAIT_MS).

OAuth token format

Server-to-Server OAuth uses a form-encoded body (grant_type=account_credentials&account_id=...) with Basic auth — see getZoomAccessToken in src/zoom/client.ts.

Attio

Workspace enablement required

Meeting and call-recording write APIs require per-workspace enablement from Attio support — correct scopes alone are not sufficient. Expect 403/404 until enabled.

external_ref is write-only

POST /v2/meetings is find-or-create keyed on external_ref, but list/GET responses never return it. The bridge uses host + start-time matching (findAttioMeetingByHostAndStart) to attach to calendar-synced meetings when possible.

Call recording from URL

POST /v2/meetings/{id}/call_recordings with { data: { video_url } }:

  • URL must be public HTTPS to an .mp4
  • Max 500MB (ATTIO_MAX_VIDEO_BYTES)
  • Attio does a HEAD first — needs valid Content-Length
  • Roughly 1 request/second rate limit

Transcript push endpoint

POST /v2/meetings/{id}/call_recordings/{id}/transcript is used by this bridge but is not in Attio's public OpenAPI spec. Payload: { data: { transcript: [{ speech, start_time, end_time, speaker }] } }. end_time must be strictly greater than start_time.

Attio does not auto-transcribe API-uploaded videos — you must push Zoom's VTT yourself.

No auto-transcription

Confirmed behavior: videos uploaded via API are not transcribed by Attio. Transcript content comes from Zoom VTT via createAttioCallRecordingTranscript.

Supabase / PostgREST

UPDATE + .or() quirk

PostgREST applies or= logic trees on UPDATE to the RETURNING representation, not the WHERE clause. A single .or('processing_started_at.is.null,...') UPDATE can set a lease while returning zero rows.

The bridge uses two sequential single-column UPDATEs (.is then .lt) in claimBridgeRowLease (src/bridge/bridge.ts). Do not "simplify" this back to one .or() call — see also the comment in sql/schema.sql.

RLS with no policies

All bridge tables have RLS enabled with no anon/authenticated policies. Only the service role key can access queue, scheduler, and dedup tables.

Storage file size limit

Raise the project storage upload limit to ≥500MB in the Supabase dashboard. The SQL schema cannot set this. Default Supabase limits are below typical hour-long recordings.

Staging bucket is private

zoom-recording-staging is created as a private bucket. Attio receives time-limited signed URLs, not public routes.

Webhook deduplication

Two-phase claim → process → commit (src/webhook-dedup.ts). Transient failures call releaseWebhookClaim so Zoom's retry re-enters the handler. sweepStaleWebhookClaims deletes rows stuck in claimed past 5 minutes.

Queue idempotency for recordings is additionally enforced by the unique index on zoom_meeting_uuid — duplicate webhook deliveries must not clobber in-flight rows (ignoreDuplicates: true on enqueue).