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)
- Open the Developer portal at app.notion.com/developers and create a connection for your workspace (type: internal).
- In the connection's tokens section, copy the secret — it starts with
ntn_. - 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://kody.codes/account/secrets/new?name=notionToken&description=Notion%20internal%20connection%20token&allowedHosts=api.notion.com&scope=userApprove the api.notion.com host on the same page after saving.
Lane B: public connection (OAuth)
- In the Developer portal, create a public connection. Choose the installation scope carefully — it cannot be changed after creation.
- On the Configuration tab, set the redirect URI to
https://kody.codes/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://kody.codes/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%7DDecoded: 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_foundfor 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 unauthorizedon 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 versionerror: add theNotion-Versionheader to every request.- Token exchange fails with
invalid_client: Notion expects HTTP Basic credentials with a JSON body. LeavetokenExchangeStyleunset so theapi.notion.comdefault (basic-json) applies. - Installation scope wrong on a public connection: it cannot be edited after creation; create a new public connection instead.
Use a personal playbook and verify
Search existing personal helpers, or save a thin wrapper with playbookSave.
Lane A uses the notionToken secret; Lane B uses the saved notion OAuth
integration. Inspect the actual export contract with playbookGet.
Run a read-only smoke test from execute without returning workspace PII. The
playbook sees only pages shared on Notion's consent screen. See
authoring; no platform helper is assumed installed.