Skip to content

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

Package listing

@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).

  1. Create an integration at https://www.notion.so/profile/integrations and set its type to Public.
  2. Set the redirect URI to https://kody.codes/connect/oauth.
  3. 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)
  4. 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 nameTypical use
notionDefault workspace
notion-personalPersonal workspace
notion-workWork workspace

Helpers accept optional account / integration:

CallResolves to
omit bothintegration 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 metadata
  • kody:@kentcdodds/notion/accounts — list connected notion / notion-* integrations
  • kody:@kentcdodds/notion/smoke-test — verify OAuth access without returning workspace or user PII
  • kody:@kentcdodds/notion/request — generic authenticated Notion API request; mutating calls require confirm: true
  • kody:@kentcdodds/notion/search — search shared pages and data sources (objectType: 'page' | 'data_source')
  • kody:@kentcdodds/notion/get-page — read a page's metadata and properties
  • kody:@kentcdodds/notion/get-block-children — read a page or block's content blocks
  • kody:@kentcdodds/notion/get-database — read a database container: title and its data sources
  • kody:@kentcdodds/notion/get-data-source — read a data source's schema/properties by data source id
  • kody:@kentcdodds/notion/query-database — query rows; accepts dataSourceId or databaseId (auto-resolves the single data source)
  • kody:@kentcdodds/notion/create-page — preview or create a page; creating requires confirm: true; parent.database_id auto-resolves to the data source
  • kody:@kentcdodds/notion/append-block-children — preview or append blocks; appending requires confirm: true; position via position: { 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-database returns the container's data_sources list; get-data-source returns properties; rows are queried via POST /data_sources/{id}/query. Helpers that take databaseId auto-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-page auto-converts parent.database_id for you.
  • Search returns data_source objects where databases used to appear; filter with objectType: 'page' | 'data_source'.
  • Block positioning uses position: { type: 'after_block', after_block: { id } } (or start / end) — the flat after parameter is gone.
  • Trash is in_trash — the archived field no longer exists in requests or responses.
  • To create an inline child database on a page (e.g. a per-page gallery), use request with POST /databases, parent: { type: 'page_id', page_id }, is_inline: true, and the schema under initial_data_source: { properties }. There is no way to create a child_database block through append-block-children.
  • Notion rewrites prop("Name") formula expressions into internal block-property references on save; don't round-trip a saved formula expression back 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.