Making Zoom Meetings Searchable: Building a Zoom Connector for cognee
Transcripts that are not ready yet, an API that only answers one month at a time, and why direct messages stay out by default.

A lot of decisions happen in meetings, and almost none of them get written down properly. Zoom does record and transcribe cloud meetings, but a folder of transcripts is not something you can ask questions.
For Mergetober, cognee’s integration hackathon, I took on issue #4731: a Zoom connector that syncs cloud recording transcripts and chats into cognee, so you can later ask “What did we decide about the release?” across every meeting. The pull request is cognee-community #277. It was the first connector I opened this month, and the review taught me a lot.
What cognee and dlt do here
cognee is an open-source memory layer for AI agents. cognee.remember(...) takes documents and builds a knowledge graph; cognee.recall(...) answers questions from it.
dlt is a Python library for loading data. The connector is a dlt resource: a generator that yields one row per meeting, plus “tombstone” rows when a meeting should be forgotten. cognee accepts it directly.
import os
import cognee
from cognee_community_connector_zoom import zoom_source
await cognee.remember(
zoom_source(
os.environ["ZOOM_ACCOUNT_ID"],
os.environ["ZOOM_CLIENT_ID"],
os.environ["ZOOM_CLIENT_SECRET"],
),
dataset_name="zoom",
primary_key="id",
write_disposition="merge", # REQUIRED, the add pipeline defaults to "replace"
max_rows_per_table=0,
self_improvement=False,
)
answer = await cognee.recall("What did we decide about the release?", datasets=["zoom"])
Authentication: Server-to-Server OAuth
Zoom offers a Server-to-Server OAuth app type for exactly this kind of background job. You create the app in the Zoom Marketplace, give it the recording and user scopes, and get an account id, client id and client secret.
The token it issues lasts one hour and there is no refresh token. The client simply requests a new one shortly before it expires, and once more if Zoom answers 401. One detail matters for security: the token is only ever sent to Zoom’s own hosts. A recording’s download link redirects to storage, and the client does not forward the token across that redirect or to any host outside Zoom.
One document per meeting instance
Each cloud recording becomes one document: topic, date, duration, host, the transcript, and the in-meeting chat. Video and audio files are never downloaded, only text.
The document is keyed by the meeting instance uuid, not the meeting number. Recurring meetings reuse their number every week, so keying on the number would make each week's meeting overwrite the last one. The reviewer specifically called out this choice as correct, along with a detail I had added on purpose: the uuid is never put into a URL path, because Zoom uuids can contain characters that need special encoding.
The one-month limit, and a smarter listing
Zoom’s recordings endpoint answers at most one month per request. To list a longer window, you have to cut it into monthly slices.

My first version stored the start of the window on the first run and listed the whole window on every run. The reviewer did the math: with 50 users and no changes, that is 100 recordings calls right after the first sync, 400 after six months, 700 after a year, and it keeps growing forever.
The fix was a two-speed listing:
- Once a day, or when you change since, a run lists the whole window, one month per request with the largest page size.
- Every other run lists only the last month, so a run with no changes costs one request per user.
The trade-off is that a change to an older meeting, or its deletion, shows up within a day instead of immediately. For meeting transcripts, that is a fair deal.
The reviewer also noticed that my test fake accepted any date range, so the month slicing was not really tested. Replacing it with one big range still passed every test. Now the fake rejects ranges over a month, exactly like the real API, and the slicing is pinned down.
Transcripts that are not ready yet
Zoom creates the transcript some minutes after a meeting ends, and only when cloud recording and audio transcription are turned on. So a sync can see a meeting whose files are still processing.
A meeting like that is skipped and checked again on the next run. But it cannot wait forever: some accounts have transcription turned off, and sometimes a file gets stuck in processing. After transcript_wait_hours (24 by default), the meeting is ingested with whatever is ready. If the transcript shows up later, the document is updated.
My first version had a bug here that the reviewer found: the 24-hour cutoff only covered the “no transcript yet” case. A file whose status stayed “processing” kept its meeting pending forever. Now the cutoff covers both.
Privacy: direct messages stay out
The saved chat file from a Zoom meeting includes direct messages between participants, not just messages to everyone. My first version only stripped the “ to Everyone” label, so a line like Sam Lee to Priya Shah(Direct Message): ... went into the dataset, readable by anyone with access to it.
That was a real privacy problem, and the reviewer flagged it. Direct messages are now left out unless you explicitly pass include_direct_messages=True, and the README explains what turning it on means.
Deleting the right things
A meeting that was synced before but is missing from a full listing is forgotten. That covers recordings you delete or move to trash, recordings removed by the account’s retention policy, and users or dates you take out of scope.
A few safety rules protect against forgetting by mistake:
- A Zoom answer that is missing its meetings or users list raises an error instead of reading as "everything was deleted". This came straight from review: the first version did page.get(key) or [], so one malformed answer would have deleted every meeting.
- A listing that is empty as a whole, while meetings were synced before, forgets nothing until the next full listing confirms it.
- Deactivated users are still listed, so deactivating someone does not read as deleting all their meetings.
- A transcript or chat file whose download link has expired (404 or 410) is skipped, and the meeting is still ingested.
Testing on a real account
I ran the connector against a real Zoom account. It was a free Basic account, and cloud recording needs a paid Pro plan, so it had no recordings. That still let me test the parts that most often break: the OAuth flow, the user listings, and the month windows.

That last row was a real find. Without the user:read:user:admin scope, Zoom answered a bare HTTP 400 (code 4711). Now the connector says exactly which scope to add. The transcript and chat file formats are covered by tests based on Zoom's API reference.
Small things that add up
The latest round fixed something easy to miss: the document shows the host’s name and the meeting’s share link, but the change check did not include them. So renaming the host or getting a new share link never updated the document. The rule I now follow is simple: whatever the document shows must be part of the change check.
Every stop is also reported clearly. A long rate limit, too many retry waits, or an expired token after some progress stops the run cleanly, keeps what finished, and sets failed with a reason in cognee_sync_stats.
What I took away from this one
- Count the requests over time, not just per run. A design that looks cheap on day one can grow without limit.
- Make your fakes as strict as the real API. If the fake accepts things the API rejects, your tests prove nothing.
- Think about who can read the dataset. Direct messages were in the data I was given, but that does not mean they belong in shared memory.
- Plan for data that arrives late. Transcripts are not instant, so the sync has to wait sensibly and then move on.
The connector has 71 tests that run on Python 3.10, 3.11 and 3.13 without a Zoom account.
Links
- Pull request: topoteretes/cognee-community #277
- Issue: topoteretes/cognee #4731
- cognee: github.com/topoteretes/cognee
Built for Mergetober 2026, cognee’s open-source integration hackathon.