All docs

Documentation

Zoom integration — install, use, and remove

Source: docs/ZOOM.md

#Zoom integration — install, use, and remove

Kindryn's Zoom integration lets community admins host events and coaching sessions on their existing Zoom subscriptions. Once connected, Kindryn schedules the meeting on Zoom, sends real iCal invites to RSVPed members, and serves a per-click join URL so members never share the raw Zoom link publicly.

This guide covers the full Zoom App Marketplace lifecycle:

  1. Prerequisites
  2. Installing — connect a Zoom account
  3. Using — hosting events and coaching sessions on Zoom
  4. Multi-account routing
  5. Webhook events Kindryn subscribes to
  6. Removing — disconnect a Zoom account
  7. Reviewer test plan (Zoom App Marketplace)
  8. Troubleshooting

#1. Prerequisites

  • A Kindryn community where you have OWNER or ADMIN role.
  • A Zoom account in good standing on a paid or free plan. Cloud recording is required if you want Kindryn to archive recordings on your behalf — free plans cannot record to the cloud, so the cloud-recording warning on the integrations page is informational.
  • Your Kindryn instance operator must have set ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET, and ZOOM_REDIRECT_URI in environment configuration. If you self-host Kindryn, follow your instance's deployment docs. If you're on the hosted kindryn.app service this is already configured.

#2. Installing — connect a Zoom account

  1. Sign in to your Kindryn community and open Admin → Integrations. The shortcut URL is /<your-community-slug>/admin/integrations.
  2. Scroll to the Video & meetings section. Click Connect Zoom.
  3. You will be redirected to Zoom's OAuth consent screen. Sign in with the Zoom account that will host meetings, and approve the requested scopes (least-privilege set per Zoom Marketplace review):
  • meeting:write:meeting — create meetings on behalf of the connected host.
  • meeting:write:registrant — add Kindryn members as registrants on RSVP so each member gets a personalized one-click join URL.
  • meeting:update:meeting — patch the scheduled meeting (topic, start time, alternative hosts).
  • meeting:update:livestream — configure the meeting's custom livestream destination when an event is bridged to the Cloudflare-Stream replay surface.
  • meeting:update:livestream_status — start / stop the configured livestream relay on meeting.started.
  • meeting:delete:meeting — cancel the meeting on Zoom when the Kindryn event is deleted or rescheduled.
  • meeting:read:meeting — required to subscribe to the meeting.started / meeting.ended webhooks Kindryn uses to mark the event live and trigger post-event hooks.
  • meeting:read:participant — required to subscribe to the meeting.participant_joined / meeting.participant_left webhooks that record per-member attendance + session duration.
  • meeting:read:meeting:admin — resolve an EXISTING meeting an organizer connects (WHP-371) when it's hosted by another user on the account (added 2026-07-23).
  • meeting:read:list_meetings:admin — list a host's upcoming meetings for the connect-existing browse picker (WHP-371, added 2026-07-23).
  • cloud_recording:read:recording — required to subscribe to the recording.completed / recording.transcript_completed webhooks (no-op if cloud recording is disabled).
  • cloud_recording:delete:meeting_recording — trash the cloud recording on Zoom after Kindryn pulls the artifact to its own storage, so your Zoom cloud quota isn't burned.
  • user:read:user — read the installer's basic profile once during OAuth callback to bind the grant to a Zoom account.
  • user:read:settings — read recording settings once to surface the "cloud recording enabled" warning in the integrations card.
  • user:read:list_users:admin — enumerate the licensed users on the connected account so the event-create flow can offer every available host (not just the installer) in the host picker.
  1. Zoom redirects you back to Kindryn. The newly connected account appears in the Video & meetings card with the email of the installing Zoom user and the list of hosts available on that Zoom account.

That's it — the account is now ready to host Kindryn-scheduled meetings.


#3. Using — hosting events and coaching sessions on Zoom

#Events

  1. In the admin shell, open a space and create or edit an event.
  2. Set Delivery mode to Zoom.
  3. Pick the Zoom host from the dropdown (this is one of the licensed hosts on your connected Zoom account).
  4. Save the event. Kindryn calls Zoom's POST /users/{host}/meetings endpoint and stores the returned meetingId and joinUrl on the event row.
  5. When members RSVP, Kindryn sends them an iCal invite via email. The calendar event contains a per-member join URL routed through Kindryn — this prevents the raw Zoom link from being shared publicly.

#Coaching sessions

The same flow applies to 1:1 coaching bookings and recurring group coaching series. Selecting Zoom as the delivery mode triggers the same meeting creation flow. Each coaching booking gets its own meeting on the coach's Zoom calendar.

Lazy provisioning for recurring series (WHP-269). Group-coaching series do NOT create a Zoom meeting for every generated session — that would park a standing meeting in the host's Zoom account for each of the ~90 days of generated sessions. Instead, each series keeps only its next N upcoming sessions pre-created ("Pre-created Zoom meetings" on the series form, default 1, max 90). A background sweep (every 15 minutes) tops the window up as sessions pass. Sessions beyond the window get their meeting created just-in-time when the first person clicks Join — a one-time ~1-second step that later joiners skip. Rescheduling or renaming a session/gathering pushes the change to its Zoom meeting when one exists; cancelling deletes it. Events and 1:1 bookings are single meetings and stay eagerly created.

#Join routing — anti-share

When a member clicks the "Join Zoom" button on an event or coaching page, Kindryn POSTs to an internal zoom-join endpoint that:

  1. Verifies the caller is the RSVPed/booked member.
  2. Verifies the meeting state (cancelled / no-show / etc.).
  3. Mints a tracked join URL and redirects.

The raw zoomJoinUrl is never sent down to the client — only the indirection URL is. This stops members from forwarding their join link to non-members.


#3b. Connecting an existing Zoom meeting (WHP-371)

When choosing Zoom delivery, organizers can connect a meeting that already exists in a connected Zoom account instead of letting Kindryn create one. Supported on every Zoom surface:

  • Events — per-event connect (paste or browse).
  • Gatherings — per-gathering connect (create + edit modals).
  • Group-coaching series — one standing meeting for the whole series: every generated session connects to it instead of provisioning per-session meetings.
  • 1:1 coaching — a space-level standing meeting: every booking in the coaching space connects to it (fresh details fetched at book time).
  • Onboarding-call slots — paste a link when creating a slot; this is the first path that fills the slot's member-facing join URL at all.

For events:

  1. In the event editor's Zoom section, choose Connect an existing meeting.
  2. Paste the Zoom invite link or numeric meeting ID — or click Browse upcoming meetings to pick from a host's schedule.
  3. Kindryn resolves the meeting, shows a preview, and stamps its join details on the event.

Kindryn is read-only on connected meetings. It never edits, deletes, or ends them, never changes their settings, never purges their cloud recordings, and title/time edits to the Kindryn event do NOT propagate to Zoom — keep the two in sync yourself. Deleting the Kindryn event or flipping it to LiveKit leaves the Zoom meeting untouched.

#Attendance trade-off

Meeting registrationJoin linksAttendance tracking
Enabled (approval type auto/manual)Per-member personalized linksAutomatic via registrant ID (reliable)
Disabled (default for most meetings)One shared generic linkOnly when a member's Zoom email matches their Kindryn email

When registration is enabled on the connected meeting, Kindryn still adds registrants for RSVPing members (an additive attendee record — the meeting itself is not modified) so per-member links and reliable attendance work. If registration is off, everyone gets the generic link and the webhook's email-matching fallback is the only attendance signal. Manual-approval registration works but registrants wait in the Zoom approval queue until the host approves them.

#Limitations

  • Webinars and personal-link URLs (zoom.us/my/…) cannot be connected — meetings only, by link with a numeric ID.
  • Livestream bridging (Stream to Kindryn watchers) is unavailable for connected meetings (it requires Zoom-side mutations).
  • A recurring external meeting connected to several Kindryn events is disambiguated by nearest start time on webhook events (±6h window) — back-to-back sessions sharing one meeting may attribute recordings to one row.
  • If the organizer deletes/recreates the meeting in Zoom, stored personalized links break; the join endpoint falls back to the generic link automatically.
  • Connected meetings exercise the admin-level read scopes meeting:read:meeting:admin (resolve a meeting hosted by any user on the account) and meeting:read:list_meetings:admin (the browse picker), plus the existing registrant scopes when registration is on. These two were added to the Marketplace app 2026-07-23 — connected accounts granted BEFORE that date must disconnect + reconnect for their token to carry them.

#4. Multi-account routing

Kindryn supports connecting multiple Zoom accounts to a single community. Each event picks a host from any connected account. This is useful when:

  • Several coaches each have their own Zoom subscription.
  • You have a production Zoom account for paid events and a free account for internal team meetings.
  • You're moving from one Zoom account to another and want both live during the transition.

To add another account, repeat step 2 with a different Zoom login. Both accounts will list in the Video & meetings card.


#5. Webhook events Kindryn subscribes to

Kindryn registers a single webhook endpoint per Zoom account:

POST https://<your-kindryn-host>/api/integrations/zoom/webhooks

Subscribed event types:

Zoom eventWhat Kindryn does
meeting.startedMarks the meeting as live in Kindryn; opens the per-event chat / Q&A.
meeting.endedMarks the meeting as ended; closes the live chat; triggers post-event hooks.
meeting.participant_joinedRecords attendance for the joining member.
meeting.participant_leftComputes duration; closes the attendance row.
recording.completedStores the cloud recording URL on the event for later playback (if enabled).
endpoint.url_validationStandard Zoom CSP challenge — Kindryn echoes the encrypted token back.

All webhook deliveries are HMAC-signed using your account's Zoom Webhook Secret. Kindryn verifies every request before processing.


#6. Removing — disconnect a Zoom account

  1. Open Admin → Integrations at /<your-community-slug>/admin/integrations.
  2. Find the Zoom account row in the Video & meetings card.
  3. Click Disconnect.

Kindryn will:

  1. Call Zoom's oauth/revoke endpoint to invalidate the access and refresh tokens on Zoom's side.
  2. Null out the stored tokens, host list, and webhook secret on the Kindryn database row.
  3. Leave any historical events and meetings intact — they will simply no longer link to a live Zoom meeting. Members who try to join an already-scheduled meeting on a disconnected account will see a "Meeting unavailable" message.

You can also uninstall the Kindryn app directly from your Zoom account at Zoom Marketplace → Manage → Installed Apps → Kindryn → Uninstall. That revokes the tokens on Zoom's side, so any Kindryn API call against the account will start failing — but uninstalling in Zoom does not, on its own, clear Kindryn's connection record. Use the Disconnect button above for a clean teardown.

#Reconnecting (and "already connected to another community")

Each Zoom account maps to exactly one community at a time. Reconnecting always works — completing the Zoom OAuth flow proves you control the Zoom account, so the community you authorize into reclaims it. This holds whether the account was previously disconnected, or is still bound to a different community (for example a shared test account during Zoom app review). The previous community simply loses the binding (same end state as a Disconnect) and its host list re-syncs under the new owner. You no longer have to disconnect from the old community first.


#7. Reviewer test plan (Zoom App Marketplace)

This section is for the Zoom Marketplace review team. Run it top-to-bottom — every requested scope is exercised by exactly one step.

#Reviewer credentials

Two ways in:

  1. Password credential — provided in the Marketplace submission form. Sign in at https://kindryn.app/auth/login.
  2. Google SSO with [email protected] — the account you mentioned in correspondence has been pre-seeded as ADMIN on the same workspace. Click "Continue with Google" on the login page.

Either path lands you in the Zoom Reviewer Workspace community at https://kindryn.app/zoom-reviewer. The workspace is pre-seeded with one space (Reviewer events) and one virtual event titled "Demo event — switch delivery to Zoom" scheduled for the next Wednesday at 11:00.

#Step 1 — Connect your Zoom account (scopes: user:read:user, user:read:settings, user:read:list_users:admin)

  1. Open https://kindryn.app/zoom-reviewer/admin/integrations.
  2. Scroll to the Video & meetings section, click Connect Zoom.
  3. Approve the OAuth consent. Zoom redirects back to Kindryn.
  4. The card now shows your Zoom email + an enumerated list of host seats on your Zoom account. (Exercises user:read:user for the installer profile, user:read:settings for the cloud-recording capability badge, user:read:list_users:admin for the host seat enumeration.)

#Step 2 — Provision a Zoom meeting on the seeded event (scopes: meeting:write:meeting, meeting:update:meeting)

  1. From the workspace home, open the Reviewer events space.
  2. Click the Demo event — switch delivery to Zoom card.
  3. Click Edit in the event header.
  4. The "Virtual event" toggle is already ON in the seed — the Delivery — where does this event run? section is visible. If you don't see a "Delivery" section, the event is not marked virtual — scroll up and turn on the Virtual event toggle, and the Delivery section appears. (Newly-created events start with the toggle OFF; the seeded reviewer event removes that hop for you, so make sure you're editing the seeded "Demo event — switch delivery to Zoom" in the Zoom Reviewer Workspace, not a fresh event.)
  5. In Delivery — where does this event run?, switch from Native (LiveKit) to Zoom.
  6. Pick a Zoom host from the dropdown.
  7. Click Save. Kindryn calls POST /users/{id}/meetings against your Zoom account (meeting:write:meeting). The event row now stores zoomMeetingId + zoomJoinUrl.
  8. Re-open the event for edit, change the title, save again. Kindryn calls PATCH /meetings/{id} (meeting:update:meeting).

#Step 3 — RSVP a member to register them on Zoom (scope: meeting:write:registrant)

  1. From the event page, click RSVP to register yourself. Kindryn calls POST /meetings/{id}/registrants so Zoom mints a personalized join URL bound to your email (meeting:write:registrant).

(Note: un-RSVPing revokes the member's Kindryn join routing immediately. Kindryn does not request meeting:update:registrant_status, so it does not cancel the registrant on Zoom's side — least-privilege; the stale Zoom registrant entry is harmless because access is gated by Kindryn, not the raw Zoom link.)

#Step 4 — Cloudflare Stream simulcast (scopes: meeting:update:livestream, meeting:update:livestream_status)

Prerequisites (why an earlier review couldn't verify these scopes):

  • These two scopes are only exercised from inside Kindryn's meeting.started webhook handler (step 3 below). They are not called on any direct button click. If webhook delivery is failing, this scope path never runs — so verifying it depends on webhooks being accepted. A signature-verification bug was 401'ing every webhook (timestamp parsed as ms instead of Zoom's epoch-seconds); that is fixed, so meeting.started now reaches the handler and these calls fire.
  • Zoom custom live streaming is a paid-plan feature. On a free Zoom account PATCH /meetings/{id}/livestream returns a plan error and the relay is a no-op. Use a host on a plan with "Allow livestreaming of meetings → Custom Live Streaming Service" enabled (Zoom Admin → Account Settings → In Meeting (Advanced)).
  1. Re-open the event Edit page.
  2. In the Zoom delivery block, toggle Simulcast to Kindryn page ON. Save. Kindryn provisions a Cloudflare Stream live input + persists the RTMPS URL on the event.
  3. Start the meeting in your Zoom client. Zoom posts meeting.started to Kindryn's webhook, which calls PATCH /meetings/{id}/livestream to set the RTMP destination (meeting:update:livestream), then PATCH /meetings/{id}/livestream/status with action=start (meeting:update:livestream_status). The event page now shows the live HLS player.

#Step 5 — Webhooks land on Kindryn (scopes: meeting:read:meeting, meeting:read:participant, cloud_recording:read:recording)

The integration auto-subscribes to meeting.started, meeting.ended, meeting.participant_joined, meeting.participant_left, recording.completed, and recording.transcript_completed. These subscriptions require meeting:read:meeting (meeting lifecycle events), meeting:read:participant (the participant_joined / participant_left attendance events), and cloud_recording:read:recording (the recording events). To exercise meeting:read:participant: join the running meeting as a participant — Kindryn records your attendance row from meeting.participant_joined; leave the meeting and it closes the row with a computed duration from meeting.participant_left. End the meeting in Zoom; Kindryn flips the event status and closes the live chat in response to meeting.ended.

#Step 6 — Trash the cloud recording after pull (scope: cloud_recording:delete:meeting_recording)

When a recording finishes processing on Zoom and Kindryn finishes pulling it down to MinIO, Kindryn calls DELETE /meetings/{uuid}/recordings?action=trash so the Zoom account's cloud-storage quota isn't consumed by content already mirrored in Kindryn (cloud_recording:delete:meeting_recording). This step requires cloud recording to be enabled on your Zoom account; if it isn't, the warning banner on the integrations card is informational and steps 5–6 around recording are no-ops.

#Step 7 — Cancel the meeting on Zoom from Kindryn (scope: meeting:delete:meeting)

  1. From the event page header, click Cancel event.
  2. Confirm. Kindryn calls DELETE /meetings/{id} on Zoom (meeting:delete:meeting).

#Step 8 — Disconnect (clean teardown)

  1. Open /zoom-reviewer/admin/integrations.
  2. Find the Zoom card. Click Disconnect.
  3. Kindryn calls Zoom's oauth/revoke endpoint, nulls out the stored tokens, and removes the host list.

#Need a paid Zoom tier?

Steps 1–3 + step 7 work on a free Zoom plan. Steps 4 (livestream) + 6 (cloud recording cleanup) need a Zoom plan that supports those features — they're no-ops on free accounts.

If you'd prefer Kindryn cover the Zoom subscription for review, reply on the submission thread and we'll provision a Zoom account on the required tier and share its credentials separately. We will not upgrade [email protected] since that's your account, not ours.


#8. Troubleshooting

#"Zoom integration is not configured on this Kindryn instance"

The instance operator has not set the required Zoom OAuth environment variables. Contact your Kindryn admin / operator and ask them to configure ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET, and ZOOM_REDIRECT_URI.

#"This Zoom account is already connected to a different community"

This no longer blocks you. Just complete the Zoom authorization again from the community you want to connect — completing OAuth reclaims the account into that community (it moves the binding off the old one). You do not need to disconnect from the other community first, and uninstalling on the Zoom Marketplace side is not required. (Older builds hard-blocked here even after a disconnect, because a stale connection record kept claiming the account; that's fixed.)

#"Cloud recording not enabled on this Zoom account"

This warning shows when the connected Zoom account doesn't have cloud recording in its plan. Meetings hosted via Kindryn will not be auto-recorded. Either upgrade your Zoom plan or ignore the warning if you don't need recording.

  1. Confirm the meeting hasn't been cancelled in Zoom directly (cancelling from Zoom doesn't propagate back to Kindryn — always cancel from the Kindryn event page).
  2. Check the event row in /admin to confirm zoomMeetingId is set.
  3. If the row exists but joins still fail, the OAuth tokens may have expired. Reconnect the Zoom account via the integrations page.

#Recording never appears on the event

Cloud recording must be enabled on the Zoom account. Also, recordings take a few minutes to process after the meeting ends — Kindryn writes the playback URL when the recording.completed webhook fires, which can be 5–15 minutes after the meeting ends depending on duration.


#Support

For Kindryn-side issues with the Zoom integration, see kindryn.app/support. For Zoom-side issues (account limits, recording policies, webhook delivery from Zoom's end), contact Zoom support directly.

#In-app join (Meeting SDK embed — WHP-330)

Zoom-delivered gatherings can join inside Kindryn's call room (facilitation rail, chair script, timer and chat all work on Zoom calls) instead of bouncing members out to the Zoom client.

  • Setup: on the same Marketplace app, enable the Meeting SDK feature and copy its Client ID/Secret into ZOOM_MEETING_SDK_KEY / ZOOM_MEETING_SDK_SECRET. Unpublished apps can join meetings hosted by the connected account — enough for a community running calls on its own Zoom. Publishing (for meetings on other accounts) requires Marketplace review; note Zoom's March 2026 policy: joining meetings _outside_ the app's account requires ZAK/OBF tokens.
  • How it works: the gathering join endpoint still JIT-registers the caller (attendance keeps mapping by registrant), then additionally returns Meeting SDK embed credentials — an HS256 SDK JWT signed server-side (server/utils/zoom-meeting-sdk.ts), the meeting number + passcode, and a fresh ZAK for hosts (organizer/facilitators) so they can _start_ the meeting from the embed. Any failure degrades to the classic join-URL.
  • Scope: desktop browsers only (Zoom's component view is not supported on mobile/tablet browsers) — small viewports and the "Open in Zoom app" escape hatch keep the classic link-out. Gallery view caps at 25 videos (Zoom web client limit). 720p send / noise suppression would additionally need cross-origin-isolation headers on the call route — deliberately deferred.
Kindryn — documentation
© 2026 Capacity in Reserve LLC. All rights reserved.