MemorySync
Integrations

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.

AI summaries

The generated summary for each meeting, with attendees and timing.

Full transcripts

Speaker-attributed transcripts, indexed separately from the summary.

Participant-scoped notes

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.

Prefer a workspace key
A workspace key belongs to the workspace rather than a person: it does not expire and keeps working after the administrator who created it leaves. Both kinds require a Business or Enterprise plan.
  • 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

Granola workspace
Approved folders
Summaries and transcripts
Outside this path: notes that are in no approved folder, and notes with no generated summary
Folder boundary
Only folders an administrator approves are read, and a note that is in no approved folder is never indexed. Private folder content is retrievable only by that meeting’s participants. Granola notes that have no generated summary yet are not available through its API at all.

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.

ItemContainsBest for
SummaryTitle, attendees, meeting time, and the generated summaryWhat was decided
TranscriptSpeaker-attributed lines for the whole meetingWhat was actually said

Guided first connection checklist

Local planning guide
Plan your Granola import

Check items for your own planning. Nothing here changes a live connection.

0 of 4 planning steps complete

Initial and incremental ingestion

  1. 1
    Add the API key

    An administrator pastes a Granola API key, which is verified before it is saved.

  2. 2
    Approve folders

    They select each folder to index; approving a folder covers its subfolders.

  3. 3
    Import history

    MemorySync reads approved folders within the configured history window.

  4. 4
    Track 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

OperationMethod and pathPythonNode.js
List selectable foldersGET …/{connection_id}/granola/available-foldersconnections.granola.available_foldersconnections.granola.availableFolders
List selected foldersGET …/{connection_id}/granola/foldersconnections.granola.foldersconnections.granola.folders
Select foldersPOST …/{connection_id}/granola/foldersconnections.granola.add_foldersconnections.granola.addFolders
Deselect a folderDELETE …/{connection_id}/granola/folders/{folder_id}connections.granola.remove_folderconnections.granola.removeFolder
Read the exclusion policyGET …/{connection_id}/granola/exclusion-policyconnections.granola.exclusion_policyconnections.granola.exclusionPolicy
Replace the exclusion policyPUT …/{connection_id}/granola/exclusion-policyconnections.granola.set_exclusion_policyconnections.granola.setExclusionPolicy
List identity mappingsGET …/{connection_id}/granola/identitiesconnections.granola.identitiesconnections.granola.identities
Link an identityPOST …/{connection_id}/granola/identities/linkconnections.granola.link_identityconnections.granola.linkIdentity
Re-run identity matchingPOST …/{connection_id}/granola/identities/relinkconnections.granola.relink_identityconnections.granola.relinkIdentity
Read settingsGET …/{connection_id}/granola/settingsconnections.granola.settingsconnections.granola.settings
Update settingsPUT …/{connection_id}/granola/settingsconnections.granola.set_settingsconnections.granola.setSettings

Authentication and scope

RequirementContract
CredentialAn API key sent as X-API-Key. Connector operations are not end-user scoped.
Read scopeintegrations:read for every GET.
Write scopeintegrations:write for every POST, PUT, PATCH and DELETE.
TenantDerived from the authenticated key. There is no tenant parameter to pass or to get wrong.
X-End-User-IDNot 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 os
from memorysync import MemorySyncClient
client = 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"])
GET/api/v2/integrations/connections/{connection_id}/granola/available-folders
200 OK
Response fieldMeaning
foldersEach with id, name, path, parent_folder_id, approved, approved_visibility, selectable and reason.
totalHow many were found.
private_notes_enabledWhether this deployment permits indexing private folders at all.
identity_coverageA 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 os
from memorysync import MemorySyncClient
client = 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)
Approve folders
POST/api/v2/integrations/connections/{connection_id}/granola/folders
200 OK
Request fieldContract
foldersRequired. A list of objects, each with id, and optionally name, parent_folder_id and visibility.
visibilityDefaults to workspace. Use private for a folder shared with a subset of people.
Response fieldMeaning
approvedThe folder records that were stored.
rejectedEntries refused, with a reason. Surface these.
sync_triggeredWhether selecting started a sync.
webhook_registeredWhether a webhook was registered so later notes arrive without polling.

3. Read back what is selected

Another bare-array response — iterate it directly.

import os
from memorysync import MemorySyncClient
client = 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"])
Read approved folders
GET/api/v2/integrations/connections/{connection_id}/granola/folders
200 OK
FieldMeaning
idThe approval record. folder_id is Granola's own id.
folder_name, folder_path, parent_folder_idWhere it sits. All nullable.
visibilityWorkspace or private, as declared.
state, state_detail, is_activeApproval state and whether future syncs read it.
notes_syncedLifetime tally of notes read across every run.
notes_indexedDistinct notes currently stored. This is the real note count.
last_updated_at, last_synced_at, authorized_atTimestamps.

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 os
from memorysync import MemorySyncClient
client = 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"])
Replace the policy
PUT/api/v2/integrations/connections/{connection_id}/granola/exclusion-policy
200 OK
Read it back
GET/api/v2/integrations/connections/{connection_id}/granola/exclusion-policy
200 OK
FieldMeaning
tenant_patternsRequest and response. What you set; the only field PUT reads.
deployment_patternsResponse only. The operator-set floor.
effective_patternsResponse 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 os
from memorysync import MemorySyncClient
client = 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,
)
Read the participant roster
GET/api/v2/integrations/connections/{connection_id}/granola/identities
200 OK
Link one participant
POST/api/v2/integrations/connections/{connection_id}/granola/identities/link
200 OK
FieldMeaning
granola_emailRequest and response. The participant's email in Granola. Required on link.
memorysync_user_idThe numeric MemorySync user to attribute notes to. Omit or send null to unlink.
granola_name, memorysync_emailResponse only. For display.
is_externalResponse only. Whether the participant is outside your organization.
match_methodResponse only. How the mapping was arrived at — automatic or explicit.
Roster fieldMeaning
identitiesThe participants.
total, matched, unmatchedCoverage. 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 os
from memorysync import MemorySyncClient
client = 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"])
POST/api/v2/integrations/connections/{connection_id}/granola/identities/relink
200 OK

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 os
from memorysync import MemorySyncClient
client = 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)
Read current settings
GET/api/v2/integrations/connections/{connection_id}/granola/settings
200 OK
Replace them
PUT/api/v2/integrations/connections/{connection_id}/granola/settings
200 OK
FieldMeaning
index_transcriptsThe only writable field. PUT requires it.
history_daysRead-only. How far back a first sync reaches.
max_notes_per_syncRead-only. The ceiling on one run.
webhook_registeredRead-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 os
from memorysync import MemorySyncClient
client = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_API_KEY"],
base_url="https://api.memorysync.io",
)
client.connections.granola.remove_folder("conn_8f21a4", "fold_b2")
DELETE/api/v2/integrations/connections/{connection_id}/granola/folders/{folder_id}
204 No Content

Errors and next action

StatusMeaningNext action
401Missing, malformed or inactive API key.Check server configuration without printing the key.
403The key lacks integrations:read or integrations:write.Grant the scope on the key, or use a key that has it.
404The connection, object or job is not visible to this tenant.Confirm the identifier belongs to this organization.
409The connection is in a state that forbids the operation.Read the connection status first and act on it.
429Rate limited, either by MemorySync or by the upstream provider.Back off; do not tighten a polling loop in response.
5xxService failure.Treat a write outcome as uncertain and reconcile by reading the connection back.
Was this page helpful?