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://heykody.app/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://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%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.
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:
- Find the listing with
community_search({ query: 'notion' })— the trusted@kody/notionlisting wraps pages, databases, and a generic request escape hatch. - Fork it with
community_fork(or click Install on the listing page). - 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. - 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.