@kentcdodds/notion
README.md
111 lines · 6.4 KB · Markdown@kentcdodds/notion
Notion logo
Intent
Provide reusable, account-agnostic Notion helpers that let Kody search, read, and query the pages and databases shared with a connected Notion integration, and make explicitly confirmed writes, with a generic escape hatch for the rest of the Notion API. Supports multiple Notion workspaces via Kody’s <provider>-<purpose> integration naming.
Required setup
Create a Notion public integration and save a Kody OAuth integration named notion (and optionally notion-<purpose> for each additional workspace).
- Create an integration at
https://www.notion.so/profile/integrationsand set its type to Public. - Set the redirect URI to
https://kody.codes/connect/oauth. - Save the integration in Kody with:
- Authorize URL:
https://api.notion.com/v1/oauth/authorize - Token URL:
https://api.notion.com/v1/oauth/token - API base:
https://api.notion.com/v1 - API host:
api.notion.com - Flow: confidential (token exchange uses Basic auth with a JSON body)
- Extra authorize params:
owner=user,response_type=code - Scopes: none (access is granted per page during the OAuth consent screen)
- Authorize URL:
- Connect or reconnect at
https://kody.codes/connect/oauth?provider=notion(or...?provider=notion-<purpose>).
Multi-workspace model
A Notion OAuth token is workspace-scoped: one connection reaches exactly one workspace. To use several workspaces, connect multiple Kody integrations:
| Integration name | Typical use |
|---|---|
notion | Default workspace |
notion-personal | Personal workspace |
notion-work | Work workspace |
Helpers accept optional account / integration:
| Call | Resolves to |
|---|---|
| omit both | integration notion (default) |
account: 'work' | integration notion-work |
integration: 'notion-work' | that exact integration |
./accounts lists whatever notion / notion-* integrations the signed-in user has connected.
What “access” means
During the OAuth flow, Notion asks which pages (or a parent page / teamspace) to share with the integration. That choice is made on Notion’s consent screen — not via OAuth scopes, and not by this package. Selecting a parent page or teamspace grants access to its children, including pages added later under that parent. Helpers can only see pages and databases the authorizing user shared with that connection. Neither Kody nor this package can widen access after connect; reconnect and choose differently on Notion’s consent screen if you need broader coverage.
Exports
kody:@kentcdodds/notion— package overview and safety metadatakody:@kentcdodds/notion/accounts— list connectednotion/notion-*integrationskody:@kentcdodds/notion/smoke-test— verify OAuth access without returning workspace or user PIIkody:@kentcdodds/notion/request— generic authenticated Notion API request; mutating calls requireconfirm: truekody:@kentcdodds/notion/search— search shared pages and data sources (objectType: 'page' | 'data_source')kody:@kentcdodds/notion/get-page— read a page's metadata and propertieskody:@kentcdodds/notion/get-block-children— read a page or block's content blockskody:@kentcdodds/notion/get-database— read a database container: title and its data sourceskody:@kentcdodds/notion/get-data-source— read a data source's schema/properties by data source idkody:@kentcdodds/notion/query-database— query rows; acceptsdataSourceIdordatabaseId(auto-resolves the single data source)kody:@kentcdodds/notion/create-page— preview or create a page; creating requiresconfirm: true;parent.database_idauto-resolves to the data sourcekody:@kentcdodds/notion/append-block-children— preview or append blocks; appending requiresconfirm: true; position viaposition: { type: 'after_block' | 'start' | 'end' }
Examples
import listNotionAccounts from 'kody:@kentcdodds/notion/accounts'
import search from 'kody:@kentcdodds/notion/search'
export default async function main() {
const { accounts } = await listNotionAccounts()
return {
accounts,
search: await search({ query: 'meeting notes', objectType: 'page', account: 'work' }),
}
}import createPage from 'kody:@kentcdodds/notion/create-page'
export default async function main() {
return createPage({
account: 'personal',
// database_id auto-resolves to the database's single data source
parent: { database_id: '00000000-0000-0000-0000-000000000000' },
properties: {
Name: { title: [{ text: { content: 'New page' } }] },
},
dryRun: true,
})
}Set confirm: true only after the user has explicitly approved the exact destination and content. A dry run never calls Notion.
API model (Notion-Version 2026-03-11)
All helpers pin Notion-Version 2026-03-11 (request accepts a notionVersion param to override for one-off calls). The modern model:
- Databases are containers; data sources hold the schema and rows.
get-databasereturns the container'sdata_sourceslist;get-data-sourcereturnsproperties; rows are queried viaPOST /data_sources/{id}/query. Helpers that takedatabaseIdauto-resolve the database's single data source and error if there are several. - Database-row pages need a data source parent (
parent: { data_source_id }).create-pageauto-convertsparent.database_idfor you. - Search returns
data_sourceobjects where databases used to appear; filter withobjectType: 'page' | 'data_source'. - Block positioning uses
position: { type: 'after_block', after_block: { id } }(orstart/end) — the flatafterparameter is gone. - Trash is
in_trash— thearchivedfield no longer exists in requests or responses. - To create an inline child database on a page (e.g. a per-page gallery), use
requestwithPOST /databases,parent: { type: 'page_id', page_id },is_inline: true, and the schema underinitial_data_source: { properties }. There is no way to create achild_databaseblock through append-block-children. - Notion rewrites
prop("Name")formula expressions into internal block-property references on save; don't round-trip a saved formulaexpressionback into a create/update payload — keep the human-readable form in your source of truth.
Pagination
List-style helpers accept pageSize (1–100) and startCursor, and return hasMore plus nextCursor for fetching the next page.