Granola integration
Retrieve AI meeting notes, summaries, and transcripts from the Granola folders an administrator approves. Recover what a meeting decided — and what was actually said — with attendees, meeting time, and a link back to the note in Granola.
What meeting knowledge becomes retrievable
Recover what a meeting decided — and what was actually said — with attendees, meeting time, and a link back to the note in Granola.
The generated summary for each meeting, with attendees and timing.
Speaker-attributed transcripts, indexed separately from the summary.
Private folder content limited to each meeting’s participants.
Connecting with an API key
Granola has no sign-in redirect. An administrator creates a key in the Granola desktop app under Settings, then Connectors, then API keys, and pastes it into MemorySync. The key is verified against Granola before the connection is saved, so a mistyped key fails immediately rather than as a silent sync failure later.
- A Granola workspace on a Business or Enterprise plan, which is what allows API keys to be created.
- A Granola API key, ideally a workspace key so it does not expire or depend on one person’s account.
- A MemorySync project for the imported meeting knowledge.
Choose folder scope
Summaries and transcripts are separate
Each meeting produces two retrievable items: the AI summary, and the full transcript. They are indexed separately so a summary edit does not force the whole transcript to be reprocessed, and so transcripts can be declined without losing summaries.
| Item | Contains | Best for |
|---|---|---|
| Summary | Title, attendees, meeting time, and the generated summary | What was decided |
| Transcript | Speaker-attributed lines for the whole meeting | What was actually said |
Guided first connection checklist
Check items for your own planning. Nothing here changes a live connection.
0 of 4 planning steps complete
Initial and incremental ingestion
- 1Add the API key
An administrator pastes a Granola API key, which is verified before it is saved.
- 2Approve folders
They select each folder to index; approving a folder covers its subfolders.
- 3Import history
MemorySync reads approved folders within the configured history window.
- 4Track new meetings
New summaries, edits, and newly shared notes arrive as they happen.
Verify with a real meeting question
Ask about a decision from a meeting in an approved folder. Confirm the result names the right meeting and links back to the note in Granola before widening folder access.
Troubleshooting and related sources
Why was the API key rejected?
Creating API keys requires a Granola Business or Enterprise plan. On Enterprise, an administrator may also need to enable API access scopes for members before a usable key can be created.
Why are no notes appearing?
Check that at least one folder is approved. Notes kept outside any folder are deliberately skipped, because being able to read a note does not establish that it is shareable.
Why is a recent meeting missing?
Granola only exposes notes that already have a generated summary. A meeting still being processed will appear on a later sync.
Why can a colleague not retrieve a private meeting?
Private folder content is limited to that meeting’s participants, and each participant must be linked to a MemorySync account.
Operations on this page
| Operation | Method and path | Python | Node.js |
|---|---|---|---|
| List selectable folders | GET …/{connection_id}/granola/available-folders | connections.granola.available_folders | connections.granola.availableFolders |
| List selected folders | GET …/{connection_id}/granola/folders | connections.granola.folders | connections.granola.folders |
| Select folders | POST …/{connection_id}/granola/folders | connections.granola.add_folders | connections.granola.addFolders |
| Deselect a folder | DELETE …/{connection_id}/granola/folders/{folder_id} | connections.granola.remove_folder | connections.granola.removeFolder |
| Read the exclusion policy | GET …/{connection_id}/granola/exclusion-policy | connections.granola.exclusion_policy | connections.granola.exclusionPolicy |
| Replace the exclusion policy | PUT …/{connection_id}/granola/exclusion-policy | connections.granola.set_exclusion_policy | connections.granola.setExclusionPolicy |
| List identity mappings | GET …/{connection_id}/granola/identities | connections.granola.identities | connections.granola.identities |
| Link an identity | POST …/{connection_id}/granola/identities/link | connections.granola.link_identity | connections.granola.linkIdentity |
| Re-run identity matching | POST …/{connection_id}/granola/identities/relink | connections.granola.relink_identity | connections.granola.relinkIdentity |
| Read settings | GET …/{connection_id}/granola/settings | connections.granola.settings | connections.granola.settings |
| Update settings | PUT …/{connection_id}/granola/settings | connections.granola.set_settings | connections.granola.setSettings |
Authentication and scope
| Requirement | Contract |
|---|---|
| Credential | An API key sent as X-API-Key. Connector operations are not end-user scoped. |
| Read scope | integrations:read for every GET. |
| Write scope | integrations:write for every POST, PUT, PATCH and DELETE. |
| Tenant | Derived from the authenticated key. There is no tenant parameter to pass or to get wrong. |
X-End-User-ID | Not used. A connection belongs to the organization, not to one end user. |
Before you start
Granola is connected with an API key: use create_with_api_key on Connection Lifecycle. Everything here assumes connection["status"] == "connected".
1. See which folders are available
Lists the folders the key can see, annotated for a picker. It also reports whether private notes are enabled and how much of the participant roster is already matched — both of which decide what is safe to select.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)available = client.connections.granola.available_folders("conn_8f21a4")print(available["total"], "folders")print("private notes enabled:", available["private_notes_enabled"])print("identity coverage:", available["identity_coverage"])for folder in available["folders"]:print(folder["id"], folder["name"], folder["selectable"], folder["reason"])
| Response field | Meaning |
|---|---|
folders | Each with id, name, path, parent_folder_id, approved, approved_visibility, selectable and reason. |
total | How many were found. |
private_notes_enabled | Whether this deployment permits indexing private folders at all. |
identity_coverage | A map of counts describing how much of the roster is mapped. |
2. Select folders, and declare their visibility
Each selected folder carries a visibility. This is the most consequential field on the page: it tells MemorySync whether a folder is a team space everyone can see or a restricted one shared with a few people, and that decides who the notes inside it can be surfaced to.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)result = client.connections.granola.add_folders("conn_8f21a4",folders=[{"id": "fold_a1", "name": "Product Team", "visibility": "workspace"},{"id": "fold_b2", "name": "Leadership", "visibility": "private"},],)print(result["sync_triggered"], result["webhook_registered"])for approved in result["approved"]:print(approved["folder_id"], approved["visibility"], approved["state"])for rejected in result["rejected"]:print("rejected:", rejected)
| Request field | Contract |
|---|---|
folders | Required. A list of objects, each with id, and optionally name, parent_folder_id and visibility. |
visibility | Defaults to workspace. Use private for a folder shared with a subset of people. |
| Response field | Meaning |
|---|---|
approved | The folder records that were stored. |
rejected | Entries refused, with a reason. Surface these. |
sync_triggered | Whether selecting started a sync. |
webhook_registered | Whether a webhook was registered so later notes arrive without polling. |
3. Read back what is selected
Another bare-array response — iterate it directly.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)for folder in client.connections.granola.folders("conn_8f21a4"):print(folder["folder_id"], folder["folder_name"], folder["visibility"], folder["state"])print(" notes synced:", folder["notes_synced"], "indexed:", folder["notes_indexed"])
| Field | Meaning |
|---|---|
id | The approval record. folder_id is Granola's own id. |
folder_name, folder_path, parent_folder_id | Where it sits. All nullable. |
visibility | Workspace or private, as declared. |
state, state_detail, is_active | Approval state and whether future syncs read it. |
notes_synced | Lifetime tally of notes read across every run. |
notes_indexed | Distinct notes currently stored. This is the real note count. |
last_updated_at, last_synced_at, authorized_at | Timestamps. |
4. Block folders by name pattern
The same pattern mechanism as Slack, applied to folder names: globs, case-insensitive, a leading # tolerated, up to 200 patterns.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)client.connections.granola.set_exclusion_policy("conn_8f21a4",tenant_patterns=["1:1*", "interview*", "personal*"],)policy = client.connections.granola.exclusion_policy("conn_8f21a4")print(policy["tenant_patterns"], policy["effective_patterns"])
| Field | Meaning |
|---|---|
tenant_patterns | Request and response. What you set; the only field PUT reads. |
deployment_patterns | Response only. The operator-set floor. |
effective_patterns | Response only. Both combined. |
5. Map participants to MemorySync users
A Granola note is a meeting with participants. Without a mapping, a note is not attributed to a person, so querying as that person will not find it. Mapping is by email.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)roster = client.connections.granola.identities("conn_8f21a4")print(roster["total"], "matched:", roster["matched"], "unmatched:", roster["unmatched"])for identity in roster["identities"]:print(identity["granola_email"], identity["is_external"], identity["match_method"])client.connections.granola.link_identity("conn_8f21a4",granola_email="sam@example.com",memorysync_user_id=4821,)
| Field | Meaning |
|---|---|
granola_email | Request and response. The participant's email in Granola. Required on link. |
memorysync_user_id | The numeric MemorySync user to attribute notes to. Omit or send null to unlink. |
granola_name, memorysync_email | Response only. For display. |
is_external | Response only. Whether the participant is outside your organization. |
match_method | Response only. How the mapping was arrived at — automatic or explicit. |
| Roster field | Meaning |
|---|---|
identities | The participants. |
total, matched, unmatched | Coverage. Drive an onboarding checklist from unmatched. |
6. Re-run the matching
Re-matches the participant roster so newly added people are picked up. It takes no arguments and creates no mappings by itself — explicit links stay explicit.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)roster = client.connections.granola.relink_identity("conn_8f21a4")print(roster["matched"], "of", roster["total"])
7. Decide whether transcripts are indexed
The settings surface is one switch plus three read-only values. index_transcripts is the significant one: a transcript is far longer than a summary and produces far more content, most of it conversational filler.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)settings = client.connections.granola.settings("conn_8f21a4")print(settings["index_transcripts"], settings["history_days"])print(settings["max_notes_per_sync"], settings["webhook_registered"])client.connections.granola.set_settings("conn_8f21a4", index_transcripts=False)
| Field | Meaning |
|---|---|
index_transcripts | The only writable field. PUT requires it. |
history_days | Read-only. How far back a first sync reaches. |
max_notes_per_sync | Read-only. The ceiling on one run. |
webhook_registered | Read-only. Whether new notes arrive by webhook rather than polling. |
8. Deselect a folder
Stops future syncs reading it. Notes and memories already imported are kept.
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",)client.connections.granola.remove_folder("conn_8f21a4", "fold_b2")
Errors and next action
| Status | Meaning | Next action |
|---|---|---|
401 | Missing, malformed or inactive API key. | Check server configuration without printing the key. |
403 | The key lacks integrations:read or integrations:write. | Grant the scope on the key, or use a key that has it. |
404 | The connection, object or job is not visible to this tenant. | Confirm the identifier belongs to this organization. |
409 | The connection is in a state that forbids the operation. | Read the connection status first and act on it. |
429 | Rate limited, either by MemorySync or by the upstream provider. | Back off; do not tighten a polling loop in response. |
5xx | Service failure. | Treat a write outcome as uncertain and reconcile by reading the connection back. |