# Notion

> Indexing a Notion workspace through the API — creating an integration, sharing pages, what gets imported, and the export-zip alternative.

- Updated: 2026-09-25
- Source: https://contextator.com/en/docs/notion/
- Language: en-US
- Author: Muhammet Şafak

---
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](#the-export-zip-alternative) at the bottom.

## 1. Create an integration

1. Go to **notion.so/profile/integrations** and create an **internal integration**.
2. Copy its token (it starts with `ntn_` or `secret_`).

## 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_KEY` must be set (32+ characters) before a source can store a token. See
> [Configuration](/en/docs/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](/en/docs/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](/en/docs/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.
