Index a Notion workspace directly through the API: every page shared with an internal integration is rendered to Markdown and kept in sync. Pages that change are re-rendered; pages that stop being shared are removed.
If you would rather not create an integration, you can also upload a Notion Export → Markdown & CSV zip — see the export route at the bottom.
1. Create an integration
- Go to notion.so/profile/integrations and create an internal integration.
- Copy its token (it starts with
ntn_orsecret_).
2. Share the pages with it
In Notion, open each page or database you want indexed and share it with the integration (… → Connections → your integration). Sharing a parent page shares its children.
Nothing is visible to Contextator until you do this — a brand-new integration sees nothing.
3. Add the source
Add source → Notion
| Field | Notes |
|---|---|
| Name | The mount prefix. Immutable |
| Integration token | Stored encrypted with SECRET_KEY, never shown again |
| Root pages or databases | Optional. Page or database ids — or simply their URLs, the id is picked out. One per line. Leave empty to take everything shared with the integration |
Press Test connection: it reads the integration’s own user and answers with its name
(Connected as Docs Importer), so a wrong token is obvious before any indexing starts.
SECRET_KEYmust be set (32+ characters) before a source can store a token. See Configuration.
What gets imported
Each page becomes one Markdown file, nested in a folder named after its parent page, with the title,
Notion id, URL and last_edited_time in frontmatter:
notion/Engineering/Runbooks/Rotating the signing key.md
Rendered block types: paragraphs, all three heading levels (including the blocks folded under a toggleable heading), bulleted, numbered and to-do lists with their nesting, quotes, callouts, code blocks, tables, dividers, equations, bookmarks, images and file links. Unknown block types are skipped rather than failing the page, so a new Notion feature never breaks an import.
Limits per source: 5000 pages and 25 levels of nesting. Requests are throttled to about three per second, which is Notion’s documented rate limit — a large workspace takes a while on the first sync and is fast afterwards.
Keeping it in sync
At the start of every index run, Contextator pulls the workspace again and re-renders only pages
whose last_edited_time changed. A page that is no longer shared, or was deleted, has its file
removed.
Notion’s own push webhook works the other way around from a git provider’s: the URL is still pasted
into a Notion subscription, the same pattern as Push Webhooks elsewhere, but
the secret runs in the opposite direction. Open a 15-minute verification window from the source’s
row first; Notion’s first delivery to that URL carries the token, which is pasted back into Notion’s
Webhooks tab to finish verification (ADR-0049). Once verified it delivers its own push webhook — but
it does not close every gap: an unshared page is noticed only on a full run. Until it is verified, or
for that gap, syncing happens when a run happens: press Sync on the source, Re-index on the
project, or call POST /api/projects/:id/reindex on a schedule — see Admin API.
When something is wrong
If the token is rejected, or none of the configured root pages can be read, the sync fails and says so. It does not report an empty workspace — which would otherwise delete every page already imported. The other sources of the project still index, and the project reports:
2/3 sources synced; notion: No configured Notion root could be read — 1a2b3c4d… (object_not_found)
If some roots are readable and others are not, the readable ones import and the failures are reported.
| Symptom | Cause and fix |
|---|---|
Test connection fails with unauthorized |
Wrong or revoked token. Create a new one and paste it |
| The source imports nothing | Nothing is shared with the integration yet. Share the pages (… → Connections) |
| A specific page is missing | It — or its parent — is not shared with the integration, or it is deeper than 25 levels |
object_not_found for a root id |
The id is wrong, or that page is not shared with the integration |
| A database’s rows are missing | Share the database itself, not only the pages inside it |
The export-zip alternative
If you cannot create an integration, export from Notion (… → Export → Markdown & CSV) and upload
the zip as an Upload files source with content type Notion export. The 32-character page id
Notion appends to every file and folder name is stripped from paths and from the links pointing at
them, and the nested Part-1.zip inside the export is unpacked automatically.
The trade-off: nothing syncs by itself — you re-export and re-upload when the content changes.