Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

← All guides

Connect Notion

Verified against the Notion console, August 2026

Notion calls integrations "connections" and manages them in its Developer portal. There are two lanes: an internal connection whose token you save as a Kody secret (fastest), or a public connection connected through the hosted OAuth flow (durable, works across workspaces).

What you get

Once connected, you can ask Kody things like:

  • "Add today's meeting notes to my Notes database."
  • "Search my Notion for the onboarding checklist and summarize it."
  • "Append this reading list to my Books page."

Before you start

  • Connections are free on every Notion plan.
  • Access is granted per page: a connection only sees pages and databases that have been explicitly shared with it (Lane A) or picked during the consent screen (Lane B). Nothing is visible by default.
  • The Developer portal lives at app.notion.com/developers; notion.so/profile/integrations redirects there.

Lane A: internal connection token (fastest)

  1. Open the Developer portal at app.notion.com/developers and create a connection for your workspace (type: internal).
  2. In the connection's tokens section, copy the secret — it starts with ntn_.
  3. In Notion, open each page or database Kody should reach, open the page menu (...) -> Connections, and add your connection. Child pages inherit access.

Save the token in Kody

Save it through the account secrets page — never paste the token into chat:

https://heykody.app/account/secrets/new?name=notionToken&description=Notion%20internal%20connection%20token&allowedHosts=api.notion.com&scope=user

Approve the api.notion.com host on the same page after saving.

Lane B: public connection (OAuth)

  1. In the Developer portal, create a public connection. Choose the installation scope carefully — it cannot be changed after creation.
  2. On the Configuration tab, set the redirect URI to https://heykody.app/connect/oauth (a self-hosted deployment registers its own origin plus /connect/oauth) and copy the client ID and client secret.

Connect to Kody

https://heykody.app/connect/oauth?provider=notion&authorizeUrl=https%3A%2F%2Fapi.notion.com%2Fv1%2Foauth%2Fauthorize&tokenUrl=https%3A%2F%2Fapi.notion.com%2Fv1%2Foauth%2Ftoken&flow=confidential&extraAuthorizeParams=%7B%22owner%22%3A%22user%22%7D

Decoded: authorize URL https://api.notion.com/v1/oauth/authorize, token URL https://api.notion.com/v1/oauth/token, flow=confidential, and extraAuthorizeParams={"owner":"user"} (the page adds response_type=code itself). No tokenExchangeStyle is needed: Kody defaults api.notion.com to basic-json — HTTP Basic credentials with a JSON body — which is what Notion's token endpoint expects. Paste the client ID and secret into the setup form, then authorize and pick the pages Kody may access.

Since June 2026, each successful authorization mints a fresh access/refresh token pair, and re-authorizing replaces the previous tokens. Kody stores the new pair automatically on reconnect (/connect/oauth?provider=notion), which is also how you change which pages are shared.

Verify

Lane A (saved secret) — run in execute after the host is approved:

export default async function main() {
	const response = await fetch('https://api.notion.com/v1/users/me', {
		headers: {
			Authorization: 'Bearer {{secret:notionToken}}',
			'Notion-Version': '2026-03-11',
		},
	})
	if (!response.ok) {
		throw new Error(
			`Notion smoke test failed: ${response.status} ${await response.text()}`,
		)
	}
	const me = (await response.json()) as { name?: string; type: string }
	return { type: me.type, name: me.name }
}

Lane B (OAuth integration):

import { createAuthenticatedFetch } from 'kody:runtime'

export default async function main() {
	const notionFetch = await createAuthenticatedFetch('notion')
	const response = await notionFetch('https://api.notion.com/v1/users/me', {
		headers: { 'Notion-Version': '2026-03-11' },
	})
	if (!response.ok) {
		throw new Error(
			`Notion smoke test failed: ${response.status} ${await response.text()}`,
		)
	}
	const me = (await response.json()) as { name?: string; type: string }
	return { type: me.type, name: me.name }
}

Every Notion API request needs the Notion-Version header.

Scopes and access

Notion has no OAuth scopes. What a connection can reach is decided by two things:

  • The pages granted: explicit Connections shares for Lane A, or the page picker on the consent screen for Lane B. Minimal tier: share one dedicated page or database. Fuller tier: share top-level pages so child pages inherit.
  • The connection's capabilities, configured in the Developer portal: read content, update content, insert content, and user-information levels. A read-content-only connection cannot write, no matter which pages it sees.

To change page access on an OAuth connection, reconnect and pick again; for an internal connection, edit the page's Connections menu.

Troubleshooting

  • object_not_found for a page you can open yourself: the page has not been shared with the connection. Add it via the page's Connections menu (Lane A) or reconnect and include it in the picker (Lane B).
  • 401 unauthorized on Lane B after re-authorizing elsewhere: each successful authorization replaces the token pair, so an older stored token stops working. Reconnect so Kody stores the current pair.
  • missing version error: add the Notion-Version header to every request.
  • Token exchange fails with invalid_client: Notion expects HTTP Basic credentials with a JSON body. Leave tokenExchangeStyle unset so the api.notion.com default (basic-json) applies.
  • Installation scope wrong on a public connection: it cannot be edited after creation; create a new public connection instead.

Fork the official package and verify

A saved integration is auth credentials only. Finish by forking the trusted official package so day-to-day work goes through maintained search, read, query, and confirmed-write helpers:

  1. Find the listing with community_search({ query: 'notion' }) — the trusted @kody/notion listing wraps pages, databases, and a generic request escape hatch.
  2. Fork it with community_fork (or click Install on the listing page).
  3. Check the fork's README Required setup: it expects an OAuth integration named notion — the default name this guide's connect link uses, so no adaptation is needed.
  4. Verify the package against your integration with its built-in smoke test from execute:
import smokeTest from 'kody:@<your-username>/notion/smoke-test'

export default async function main() {
	return smokeTest()
}

A successful response confirms OAuth access without returning workspace PII — proving the fork, the tokens, and the page grants all line up. Remember the package only sees pages you shared on Notion's consent screen.

Working with an agent? This guide is also plain markdown at /guides/notion.md, or load it over MCP with coding_guide_get({ guide: 'provider_notion' }).