diff --git a/skills/productivity/notion/SKILL.md b/skills/productivity/notion/SKILL.md index 83222ffd9384d..49ea683a93fe6 100644 --- a/skills/productivity/notion/SKILL.md +++ b/skills/productivity/notion/SKILL.md @@ -1,448 +1,374 @@ --- name: notion -description: "Notion API + ntn CLI: pages, databases, markdown, Workers." -version: 2.0.0 -author: community +description: "Use when reading, writing, integrating, or troubleshooting Notion through the REST API, ntn CLI, MCP, webhooks, pages, data sources, markdown, blocks, comments, files, or official JS SDK." +version: 2.1.0 +author: community + Hermes Agent license: MIT platforms: [linux, macos, windows] prerequisites: env_vars: [NOTION_API_KEY] metadata: hermes: - tags: [Notion, Productivity, Notes, Database, API, CLI, Workers] + tags: [Notion, Productivity, Notes, Data Sources, API, CLI, Markdown, Files, Webhooks, MCP] homepage: https://developers.notion.com + related_skills: [web-apis, oauth-sota, webhook-subscriptions] --- # Notion -Talk to Notion two ways. Same integration token works for both — pick by what's available. +## Overview -◆ **`ntn` CLI** — Notion's official CLI. Shorter syntax, one-line file uploads, required for Workers. macOS + Linux only as of May 2026 (Windows support "coming soon"). **Default when installed.** -◆ **HTTP + curl** — works everywhere including Windows. **Default fallback** when `ntn` isn't installed. +Use this skill for Notion's developer surfaces: the Notion REST API, `ntn` CLI, official JS/TS SDK, webhooks, MCP, page markdown, blocks, data sources, comments, views, and file uploads. -## Setup +This skill was refreshed from official Notion docs on 2026-05-18. Highest-authority sources are: -### 1. Get an integration token (required for both paths) +1. `https://developers.notion.com/openapi.json` — public OpenAPI 3.1 spec for the documented REST API. +2. `https://developers.notion.com/llms.txt` and official `.md` pages under `developers.notion.com`. +3. `https://github.com/makenotion/notion-sdk-js` / `@notionhq/client` README. +4. `ntn` CLI docs and Notion product/dev pages for Workers, MCP, and alpha/beta surfaces. -1. Create an integration at https://notion.so/my-integrations -2. Copy the API key (starts with `ntn_` or `secret_`) -3. Store in `~/.hermes/.env`: - ``` - NOTION_API_KEY=ntn_your_key_here - ``` -4. **Share target pages/databases with the integration** in Notion: page menu `...` → `Connect to` → your integration name. Without this, the API returns 404 for that page even though it exists. +Default REST API version for new code: **`2026-03-11`**. -### 2. Install `ntn` (preferred path on macOS / Linux) +Important version cliffs: -```bash -# Recommended -curl -fsSL https://ntn.dev | bash +- `2025-09-03`: Notion databases became containers; rows/schema live in **data sources**. +- `2026-03-11`: use `position` instead of legacy `after` for block insertion; use `in_trash` instead of `archived`; use `meeting_notes` instead of `transcription`. -# Or via npm (needs Node 22+, npm 10+) -npm install --global ntn +## When to Use -ntn --version # verify -``` +Use this skill when the task mentions: -**Skip `ntn login` — use the integration token instead.** This works headlessly, no browser needed: -```bash -export NOTION_API_TOKEN=$NOTION_API_KEY # ntn reads NOTION_API_TOKEN -export NOTION_KEYRING=0 # don't try to use the OS keychain -``` +- Notion pages, databases, data sources, views, blocks, comments, users, search, files, markdown, webhooks, MCP, Workers, or `ntn`. +- `api.notion.com`, `Notion-Version`, `@notionhq/client`, `@notionhq/workers`, `mcp.notion.com`, or `NOTION_API_KEY` / `NOTION_API_TOKEN`. +- Building a Notion integration, syncing Notion data, exporting/importing content, receiving Notion events, or troubleshooting Notion permissions. -Add those exports to your shell profile (or to `~/.hermes/.env`) so every session inherits them. +Do not use this skill for generic note-taking advice unrelated to the API. -### 3. Choose path at runtime +## Credentials and Setup -```bash -if command -v ntn >/dev/null 2>&1; then - # use ntn -else - # fall back to curl -fi -``` +### Hermes env variable -Windows users: skip step 2 entirely until native `ntn` ships — Path B works fine. If you want CLI ergonomics now, install `ntn` inside WSL2. +Hermes currently treats `NOTION_API_KEY` as the configured secret name. Keep that as the primary Hermes prerequisite. -## API Basics +`~/.hermes/.env` uses dotenv-style literal `KEY=value` lines. Do **not** put `export ...` lines in it, and do **not** rely on `$NOTION_API_KEY` expansion inside the file. -`Notion-Version: 2025-09-03` is required on all HTTP requests. `ntn` handles this for you. In this version, what users call "databases" are called **data sources** in the API. +```dotenv +NOTION_API_KEY=ntn_xxx_or_secret_xxx +``` -## Path A — `ntn` CLI (preferred, macOS / Linux) +For `ntn`, Notion's own CLI reads `NOTION_API_TOKEN`. If you need both Hermes and `ntn`, duplicate the literal token: -### Raw API calls (shorthand for curl) -```bash -ntn api v1/users # GET -ntn api v1/pages parent[page_id]=abc123 \ # POST with inline body - properties[title][0][text][content]="Notes" -ntn api v1/pages/abc123 -X PATCH archived:=true # PATCH; := is non-string (bool/num/null) +```dotenv +NOTION_API_KEY=ntn_xxx_or_secret_xxx +NOTION_API_TOKEN=ntn_xxx_or_secret_xxx ``` -Syntax notes: -- `key=value` — string fields -- `key[nested]=value` — nested object fields -- `key:=value` — typed assignment (booleans, numbers, null, arrays) +Tokens are opaque strings. Newer public API tokens use the `ntn_` prefix; old `secret_` tokens can still work. Do not regex-validate token formats. -### Search -```bash -ntn api v1/search query="page title" -``` +### Choose a client path -### Read page metadata -```bash -ntn api v1/pages/{page_id} -``` +Prefer by availability: -### Read page as Markdown (agent-friendly) -```bash -ntn api v1/pages/{page_id}/markdown -``` +1. **`ntn` CLI** on macOS/Linux for one-shot terminal work, file uploads, endpoint inspection, and Workers commands. +2. **HTTP + curl** everywhere, including Windows and minimal environments. +3. **Official JS/TS SDK** for TypeScript/Node integrations. +4. **Hosted Notion MCP** for interactive AI-user access, not headless bearer-token automation. -### Read page content as blocks -```bash -ntn api v1/blocks/{page_id}/children -``` +`ntn` install: -### Create page from Markdown ```bash -ntn api v1/pages \ - parent[page_id]=xxx \ - properties[title][0][text][content]="Notes from meeting" \ - markdown="# Agenda - -- Q3 roadmap -- Hiring" +curl -fsSL https://ntn.dev | bash +# or, with Node 22+ and npm 10+ +npm install --global ntn +ntn --version ``` -### Patch a page with Markdown -```bash -ntn api v1/pages/{page_id}/markdown -X PATCH \ - markdown="## Update +`ntn` is documented for macOS/Linux x64/arm64; Windows support was still listed as “coming soon” in the 2026-05-18 docs. Use curl on Windows, or use `ntn` in WSL2. -Shipped the prototype." -``` +### Minimal HTTP smoke test -### Query a database (data source) -```bash -ntn api v1/data_sources/{data_source_id}/query -X POST \ - filter[property]=Status filter[select][equals]=Active -``` +Only run this if a real token is intentionally configured: -For complex queries with `sorts`, multiple filter clauses, or compound logic, pipe JSON in: ```bash -echo '{"filter": {"property": "Status", "select": {"equals": "Active"}}, "sorts": [{"property": "Date", "direction": "descending"}]}' | \ - ntn api v1/data_sources/{data_source_id}/query -X POST --json - +curl -sS "https://api.notion.com/v1/users/me" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" ``` -### File uploads (one-liner — biggest CLI win) +For JSON bodies add: + ```bash -ntn files create < photo.png -ntn files create --external-url https://example.com/photo.png -ntn files list + -H "Content-Type: application/json" ``` -Compare to the 3-step HTTP flow (create upload → PUT bytes → reference). +## Auth and Access Model -### Useful env vars -| Var | Effect | -|---|---| -| `NOTION_API_TOKEN` | Auth token (overrides keychain) — set this to your integration token | -| `NOTION_KEYRING=0` | File-based creds at `~/.config/notion/auth.json` instead of OS keychain | -| `NOTION_WORKSPACE_ID` | Skip the workspace picker prompt | +REST requests use bearer-token auth plus a mandatory `Notion-Version` header. -## Path B — HTTP + curl (cross-platform, default on Windows) +Token types: -All requests share this pattern: +- **Internal connection token**: static bot token for one workspace. It has no content access by default. Grant access from the Developer Portal Content access tab or in Notion UI by adding the connection to a page/database. Parent access flows to children. +- **Public connection token**: OAuth 2.0 access token per authorizing user/workspace. Use `state` for CSRF/app-state. Store `bot_id` as the primary authorization key. Refreshing returns a new access token and a new refresh token. +- **Personal access token (PAT)**: static user-scoped token. Acts as the user who created it, expires after one year, and follows that user's workspace/page permissions. Good for trusted scripts and CLI; use OAuth for multi-user products. -```bash -curl -s -X GET "https://api.notion.com/v1/..." \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" -``` +Capabilities matter and do not override page/workspace permissions: -On Windows the `curl` shipped with Windows 10+ works as-is. PowerShell users can also use `Invoke-RestMethod`. +- read content: retrieve/query pages, blocks, data sources, markdown. +- insert content: create pages/data sources, append blocks, attach uploaded files. +- update content: update/trash/restore pages/blocks, markdown updates, schema changes where permitted. +- read/insert comments: comment APIs. +- user info capabilities: user list/retrieve and optional email visibility. -### Search -```bash -curl -s -X POST "https://api.notion.com/v1/search" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"query": "page title"}' -``` +Gotchas: -### Read page metadata -```bash -curl -s "https://api.notion.com/v1/pages/{page_id}" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" -``` +- `404 object_not_found` often means “not shared with this token/connection,” not true absence. +- `403 restricted_resource` means missing capability or permission. +- Relation targets and linked data sources usually must also be shared with the connection. +- PATs cannot list all workspace users; retrieve the token's bot/current user instead. -### Read page as Markdown (agent-friendly) +## API Basics -Easier to feed to a model than block JSON. +Base URL: -```bash -curl -s "https://api.notion.com/v1/pages/{page_id}/markdown" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" +```text +https://api.notion.com ``` -### Read page content as blocks (when you need structure) -```bash -curl -s "https://api.notion.com/v1/blocks/{page_id}/children" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" -``` +Public OpenAPI spec: -### Create page from Markdown +```text +https://developers.notion.com/openapi.json +``` -`POST /v1/pages` accepts a `markdown` body param. +Undocumented/lower-authority spec: -```bash -curl -s -X POST "https://api.notion.com/v1/pages" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "parent": {"page_id": "xxx"}, - "properties": {"title": [{"text": {"content": "Notes from meeting"}}]}, - "markdown": "# Agenda\n\n- Q3 roadmap\n- Hiring\n\n## Decisions\n- Ship MVP Friday" - }' +```text +https://developers.notion.com/openapi-undocumented.json ``` -### Patch a page with Markdown -```bash -curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}/markdown" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"markdown": "## Update\n\nShipped the prototype."}' -``` +Rules: -### Create page in a database (typed properties) -```bash -curl -s -X POST "https://api.notion.com/v1/pages" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "parent": {"database_id": "xxx"}, - "properties": { - "Name": {"title": [{"text": {"content": "New Item"}}]}, - "Status": {"select": {"name": "Todo"}} - } - }' -``` +- Use HTTPS and JSON unless the endpoint explicitly says multipart/form-data. +- `Notion-Version: 2026-03-11` for new REST work. +- The URL namespace remains `/v1`; date-versioning is only the header. +- IDs are UUIDs; Notion accepts dashed or undashed forms. +- Empty strings are not supported. Use `null` to unset nullable strings. +- Ignore unknown response fields; additive changes can occur without version bumps. +- Treat cursors as opaque. Pass `next_cursor` back as `start_cursor`; never parse it. +- For full endpoint/current-schema inventory, load `references/openapi-generated-inventory-2026-05-18.md`; for drift/codegen traps, load `references/deep-edge-cases-and-codegen.md`. -### Query a database (data source) -```bash -curl -s -X POST "https://api.notion.com/v1/data_sources/{data_source_id}/query" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "filter": {"property": "Status", "select": {"equals": "Active"}}, - "sorts": [{"property": "Date", "direction": "descending"}] - }' -``` +## Common Task Recipes + +### Search for pages or data sources by title + +Use search as discovery, not authoritative inventory. Search is eventually consistent and not exhaustive. -### Create a database ```bash -curl -s -X POST "https://api.notion.com/v1/data_sources" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ +curl -sS -X POST "https://api.notion.com/v1/search" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ -H "Content-Type: application/json" \ - -d '{ - "parent": {"page_id": "xxx"}, - "title": [{"text": {"content": "My Database"}}], - "properties": { - "Name": {"title": {}}, - "Status": {"select": {"options": [{"name": "Todo"}, {"name": "Done"}]}}, - "Date": {"date": {}} - } - }' + -d '{"query":"roadmap","filter":{"property":"object","value":"page"},"page_size":10}' ``` -### Update page properties +For database-like objects in `2025-09-03+`, search/filter results use `data_source`, not `database`. + +### Read a page for an agent + +Prefer markdown for model-readable page content: + ```bash -curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"properties": {"Status": {"select": {"name": "Done"}}}}' +curl -sS "https://api.notion.com/v1/pages/${PAGE_ID}/markdown" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" ``` -### Append blocks to a page +If `truncated` is true or `unknown_block_ids` are returned, fetch those block/page IDs or fall back to structured block traversal. + +### Query a data source + +Database containers are not row tables anymore. Discover the data source ID first from `GET /v1/databases/{database_id}`, then query: + ```bash -curl -s -X PATCH "https://api.notion.com/v1/blocks/{page_id}/children" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ +curl -sS -X POST "https://api.notion.com/v1/data_sources/${DATA_SOURCE_ID}/query" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ -H "Content-Type: application/json" \ - -d '{ - "children": [ - {"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": "Hello from Hermes!"}}]}} - ] - }' + -d '{"page_size":50}' ``` -### File uploads (3-step flow) -```bash -# 1. Create upload -curl -s -X POST "https://api.notion.com/v1/file_uploads" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"filename": "photo.png", "content_type": "image/png"}' +For very large data sources, avoid blind full polling. Query pagination can cap around 10,000 results; use filters and webhooks. -# 2. PUT bytes to the upload_url returned above -curl -s -X PUT "{upload_url}" --data-binary @photo.png +### Work with saved views -# 3. Reference {file_upload_id} in a page/block payload -``` +Use views when the user's saved filter/sort/layout is the desired truth. Use data-source query when you need ad-hoc filters/sorts. -## Property Types +- List views with `GET /v1/views?database_id=...` or `GET /v1/views?data_source_id=...`; retrieve full configuration with `GET /v1/views/{view_id}`. +- Create views with `POST /v1/views` using `data_source_id` plus exactly one placement parent: `database_id`, dashboard `view_id`, or `create_database`. +- View queries are cached result sets: create `/queries`, paginate, then delete/free them. They expire after about 15 minutes and cannot accept extra filters/sorts. +- Dashboard view `configuration.rows` is read-only; change dashboard layout by creating/deleting widget views, respecting the documented widget-row limits. -Common property formats for database items: +### Create a page from markdown -- **Title:** `{"title": [{"text": {"content": "..."}}]}` -- **Rich text:** `{"rich_text": [{"text": {"content": "..."}}]}` -- **Select:** `{"select": {"name": "Option"}}` -- **Multi-select:** `{"multi_select": [{"name": "A"}, {"name": "B"}]}` -- **Date:** `{"date": {"start": "2026-01-15", "end": "2026-01-16"}}` -- **Checkbox:** `{"checkbox": true}` -- **Number:** `{"number": 42}` -- **URL:** `{"url": "https://..."}` -- **Email:** `{"email": "user@example.com"}` -- **Relation:** `{"relation": [{"id": "page_id"}]}` +Under a normal page: -## API Version 2025-09-03 — Databases vs Data Sources +```bash +curl -sS -X POST "https://api.notion.com/v1/pages" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"parent":{"type":"page_id","page_id":"PAGE_ID"},"markdown":"# Notes\n\n- Decision: ship"}' +``` -- **Databases became data sources.** Use `/data_sources/` endpoints for queries and retrieval. -- **Two IDs per database:** `database_id` and `data_source_id`. - - `database_id` when creating pages: `parent: {"database_id": "..."}` - - `data_source_id` when querying: `POST /v1/data_sources/{id}/query` -- Search returns databases as `"object": "data_source"` with the `data_source_id` field. +Under a data source, use `data_source_id` as parent and provide properties matching the schema. -## Notion Workers (advanced, requires `ntn`) +### Create or apply page templates -Workers are TypeScript programs Notion hosts for you. One worker can expose any combination of: -- **Syncs** — pull data from external APIs into a Notion database on a schedule (default 30 min). -- **Tools** — appear as callable tools inside Notion's Custom Agents. -- **Webhooks** — receive HTTP events from external services (GitHub, Stripe, etc.) and act in Notion. +- List data-source templates with `GET /v1/data_sources/{data_source_id}/templates`; templates are ordinary Notion pages surfaced for that data source. +- For page create/update, `template` is `{ "type": "default" }` or `{ "type": "template_id", "template_id": "..." }`; include `timezone` when `@now` / `@today` should resolve predictably. +- Template application is asynchronous: read the page after the request before depending on merged content or properties. +- Do not combine `template` with `children`; avoid mixing template with markdown/content modes unless the current schema docs prove that exact combination. +- Updating an existing page with `erase_content: true` is destructive replacement before template application. -**Plan / platform gating:** -- CLI works on all plans. **Deploying Workers requires Business or Enterprise.** -- `ntn` is macOS/Linux only as of May 2026. Windows users need WSL2 or to wait for native support. -- Free through August 11, 2026; metered on Notion credits after. +### Update page markdown -### Minimal Worker +Prefer exact search/replace updates with `update_content`, or whole-page replacement with `replace_content`. Legacy `insert_content` and `replace_content_range` still exist but are not preferred. ```bash -ntn workers new my-worker # scaffold -cd my-worker -# Edit src/index.ts -ntn workers deploy --name my-worker +curl -sS -X PATCH "https://api.notion.com/v1/pages/${PAGE_ID}/markdown" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"command":{"type":"update_content","content_updates":[{"old_str":"Status: draft","new_str":"Status: final"}]}}' ``` -`src/index.ts`: -```typescript -import { Worker } from "@notionhq/workers"; +If the old string matches multiple places, set `replace_all_matches: true` deliberately. If the page changed, exact matching fails instead of silently editing the wrong text. -const worker = new Worker(); -export default worker; +### Append blocks -worker.tool("greet", { - title: "Greet a User", - description: "Returns a friendly greeting", - inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, - execute: async ({ name }) => `Hello, ${name}!`, -}); -``` +Use `position`, not legacy `after`: -### Webhook capability - -```typescript -worker.webhook("onGithubPush", { - title: "GitHub Push Handler", - execute: async (events, { notion }) => { - for (const event of events) { - // event.body, event.rawBody (for signature verification), event.headers - console.log("got delivery", event.deliveryId); - } - }, -}); +```bash +curl -sS -X PATCH "https://api.notion.com/v1/blocks/${BLOCK_ID}/children" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"position":{"type":"end"},"children":[{"object":"block","type":"paragraph","paragraph":{"rich_text":[{"text":{"content":"Hello from Hermes"}}]}}]}' ``` -After deploy: `ntn workers webhooks list` shows the URL Notion generates. Treat that URL as a secret — anyone with it can POST events unless you add signature verification. +Limits: max 100 block children per append request, max two nesting levels in one request. + +### File uploads -### Worker lifecycle commands +Prefer `ntn` when installed: ```bash -ntn workers deploy -ntn workers list -ntn workers exec -d '{"name": "world"}' -ntn workers sync trigger # run a sync now -ntn workers sync pause -ntn workers env set GITHUB_WEBHOOK_SECRET=... -ntn workers runs list # recent invocations -ntn workers runs logs -ntn workers webhooks list +ntn files create < ./photo.png ``` -When asked to build a Worker, scaffold with `ntn workers new`, write the code in `src/index.ts`, set any secrets with `ntn workers env set`, and deploy. Notion's docs at https://developers.notion.com/workers cover the full API surface. +HTTP flow is not a PUT-to-presigned-URL flow. It is: -## Notion-Flavored Markdown (used by `/markdown` endpoints) +1. `POST /v1/file_uploads` to create a File Upload object. +2. `POST /v1/file_uploads/{file_upload_id}/send` with multipart form field `file`. +3. Attach `{ "type": "file_upload", "file_upload": { "id": "..." } }` in a supported page/block/property API. -Standard CommonMark plus XML-like tags for Notion-specific blocks. Use **tabs** for indentation. +Attach uploaded files within one hour. File download URLs expire after one hour; re-fetch the file/page/block object to refresh URLs. -**Blocks beyond CommonMark:** -``` - - Ship the MVP by **Friday**. - +## Webhooks, MCP, Workers, SDK -
-Toggle title - Children indented one tab -
+Webhooks: - - Left side - Right side - +- Created in the connection's Developer Portal Webhooks tab, not by a REST endpoint in the public OpenAPI spec. +- Target must be public HTTPS; localhost is not reachable. +- Verification POST includes `verification_token`; paste it into the portal to activate. +- Validate event bodies with `X-Notion-Signature`: HMAC-SHA256 over the exact raw JSON body using the subscription verification token. +- Events are signals. Fetch latest state by REST API; ordering is not guaranteed. +- Notion retries failed deliveries up to 8 times with exponential backoff, with final retry around 24 hours after the first event. +- Webhook subscriptions have their own Developer Portal API version. Upgrade handlers deliberately: `2025-09-03` changes database/data-source event shapes, while `2026-03-11` webhook payloads are documented as identical to `2025-09-03`; REST `archived` → `in_trash` does not apply to webhook payload fields. - -``` +MCP: -**Inline:** -- Mentions: ``, `Title`, `` -- Underline: `text` -- Color: `text` or block-level `{color="blue"}` on the first line -- Math: inline `$x^2$`, block `$$ ... $$` -- Citations: `[^https://example.com]` +- Hosted endpoints: `https://mcp.notion.com/mcp` and legacy `https://mcp.notion.com/sse`. +- Requires user OAuth; it does not support bearer-token headless auth. +- MCP access equals the authorizing Notion user's access. +- File uploads are not currently supported by hosted Notion MCP; use File Upload API. -**Colors:** `gray brown orange yellow green blue purple pink red`, plus `*_bg` variants for backgrounds. +Workers / Developer Platform: -Headings 5/6 collapse to H4. Multiple `>` lines render as separate quote blocks — use `
` inside a single `>` for multi-line quotes. +- `ntn` has Workers commands for scaffold/deploy/list/exec/sync/env/oauth/runs/webhooks. +- Workers are hosted TypeScript programs with syncs, tools, and incoming webhooks. Treat deeper Worker/Agent SDK docs as active beta/alpha surfaces; re-check official docs before production work. -## Choosing the Right Path +JS/TS SDK: -| Task | mac / Linux | Windows | -|---|---|---| -| Read/write pages, search, query databases | `ntn api ...` | curl | -| Read a page for an agent to summarize | `ntn api v1/pages/{id}/markdown` | curl `/markdown` endpoint | -| Upload a file | `ntn files create < file` | 3-step HTTP flow | -| One-off API exploration | `ntn api ...` | curl | -| Build a sync / webhook / agent tool hosted by Notion | `ntn workers ...` | WSL2 + `ntn workers ...` | +```bash +npm install @notionhq/client +``` + +SDK v5+ supports `2025-09-03` and `2026-03-11`, but defaults to `2025-09-03`. Opt into latest explicitly: -## Notes +```javascript +const { Client } = require("@notionhq/client"); +const notion = new Client({ + auth: process.env.NOTION_API_KEY, + notionVersion: "2026-03-11", +}); +``` -- Page/database IDs are UUIDs (with or without dashes — both accepted). -- Rate limit: ~3 requests/second average. The CLI doesn't bypass this. -- The API cannot set database **view** filters — that's UI-only. -- Use `"is_inline": true` when creating data sources to embed them in a page. -- Always pass `-s` to curl to suppress progress bars (cleaner agent output). -- Pipe JSON through `jq` when reading: `... | jq '.results[0].properties'`. -- Notion also ships an MCP server now (`Notion MCP`, ~91% more token-efficient on DB ops than the previous version) — wire it via Hermes' MCP support if you want streaming Notion access from inside a session, but the paths above are enough for most one-shot tasks. +SDK retries 429 for all methods and 500/503 for idempotent GET/DELETE by default; still design caller-level idempotency for creates. + +## Request Mechanics + +- Rate limit: average 3 requests/second per connection; respect `Retry-After` on 429. +- Payload limit: 500KB overall and 1000 block elements per request. +- Array limits: many block/rich-text arrays cap at 100 elements. +- Rich text content/link URL: 2000 chars; equation: 1000 chars; URL: 2000 chars; email/phone: 200 chars; relation/people: 100 entries. +- `GET` paginated endpoints take query params; `POST` paginated endpoints take JSON body params. +- `page_size` max is generally 100. +- Error response programmatic field is `code`; message text may change without version bump. +- Retry transient 502/503/504 with backoff and jitter. For data-source query 503, reduce `page_size` and narrow filters/sorts. +- Do not blind-retry `POST /v1/pages` or `POST /v1/file_uploads` after ambiguous network failures; no idempotency-key is documented. + +## References + +Load these support files for deeper work: + +- `references/api-knownness-packet.md` — web-apis knownness packet and handoff summary. +- `references/official-source-map.md` — source ranking, OpenAPI/spec receipts, monitor candidates, Stockitup/local state. +- `references/setup-auth-and-cli.md` — tokens, OAuth/PAT/internal connections, `ntn`, curl, SDK setup. +- `references/api-2026-03-11.md` — version cliffs, endpoint map, pagination/errors/limits. +- `references/data-sources-and-pages.md` — data source/database/page/block/property/view model. +- `references/markdown-workflows.md` — page markdown create/read/update and enhanced markdown syntax. +- `references/block-types.md` — block payload examples and traversal/update rules. +- `references/file-uploads.md` — File Upload API and `ntn files` workflows. +- `references/webhooks-mcp-workers-sdk.md` — webhooks, MCP, Workers, JS SDK, monitor gaps. +- `references/openapi-generated-inventory-2026-05-18.md` — generated endpoint/operation/webhook/schema inventory from official OpenAPI. +- `references/deep-edge-cases-and-codegen.md` — second-pass edge cases, docs drift, codegen rules, and monitor inputs. +- `scripts/notion_api_surface_snapshot.py` — no-credential public docs/spec/package snapshot tool for drift monitoring. + +## Common Pitfalls + +1. **Using stale `2025-09-03` examples for new code.** Use `2026-03-11` unless compatibility requires an older version. +2. **Calling databases rows.** Databases are containers; data sources hold schema and rows. +3. **Using `database_id` for new row parents/relations.** Use `data_source_id` after `2025-09-03`. +4. **Using `archived`, `after`, or `transcription` in new REST payloads.** Use `in_trash`, `position`, and `meeting_notes`; treat meeting-notes blocks as read/query-only, not create/update payload targets. +5. **Assuming 404 means absent.** It often means the page/data source is not shared with the token owner/connection. +6. **Forgetting capabilities.** Token page access is not enough if the connection lacks read/insert/update/comment/user capability. +7. **Using search as inventory.** Search is eventually consistent and not exhaustive. Prefer known IDs or data-source queries. +8. **Pasting shell exports into `.env`.** Hermes `.env` wants literal `KEY=value` lines. +9. **Copying malformed docs/examples blindly.** Normalize curl Authorization headers and version headers yourself. +10. **Caching signed file URLs.** They expire; re-fetch objects for fresh URLs. +11. **Treating MCP as headless integration auth.** Hosted MCP is OAuth-user oriented; use REST/SDK/Workers for unattended automation. +12. **Retrying creates without dedupe.** Notion does not document idempotency keys for create APIs. + +## Verification Checklist + +- [ ] Chosen source: OpenAPI/spec, official `.md` docs, SDK README, CLI docs, or explicitly lower-authority product/beta docs. +- [ ] REST calls include `Authorization: Bearer …` and `Notion-Version: 2026-03-11`. +- [ ] Content access was granted/shared to the internal/public connection, or a PAT is intentionally used. +- [ ] Required capabilities match the endpoint. +- [ ] Data-source work uses `data_source_id`; old database endpoints are treated as deprecated/compat paths. +- [ ] Pagination follows `has_more` / `next_cursor`; cursors are opaque. +- [ ] 429 honors `Retry-After`; transient 502/503/504 use backoff; unsafe creates are deduped. +- [ ] File uploads attach within one hour and signed file URLs are not cached long-term. +- [ ] Webhook receiver validates `X-Notion-Signature` before production use. +- [ ] No credentials, webhook tokens, page customer data, or file URLs are pasted into durable public artifacts. diff --git a/skills/productivity/notion/references/api-2026-03-11.md b/skills/productivity/notion/references/api-2026-03-11.md new file mode 100644 index 0000000000000..0b2d7c2c2b3a8 --- /dev/null +++ b/skills/productivity/notion/references/api-2026-03-11.md @@ -0,0 +1,218 @@ +# Notion API 2026-03-11 Operating Map + +Sources: + +- `https://developers.notion.com/openapi.json` +- `https://developers.notion.com/reference/versioning.md` +- `https://developers.notion.com/reference/changes-by-version.md` +- `https://developers.notion.com/guides/get-started/upgrade-guide-2026-03-11.md` +- `https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03.md` +- `https://developers.notion.com/reference/request-limits.md` +- `https://developers.notion.com/reference/status-codes.md` + +## Required envelope + +```text +Authorization: Bearer +Notion-Version: 2026-03-11 +Content-Type: application/json # when sending JSON body +``` + +Rules: + +- Base URL: `https://api.notion.com`. +- REST path namespace remains `/v1`. +- Request/response bodies are JSON except file upload send endpoints, which use multipart/form-data. +- IDs are UUIDs and may be dashed or undashed. +- Properties use snake_case. +- Date/datetime values are ISO 8601. +- Use `null`, not empty string, to unset nullable strings. +- Ignore unknown response fields. + +## Version cliffs + +### `2025-09-03` + +Databases changed shape: + +- `database` is a container. +- `data_source` is a table/schema/row parent. +- Retrieve a database to discover `data_sources`. +- Retrieve/query/update the data source for schema and rows. +- Search object filter/result value is `data_source` instead of `database`. +- Page row parents and relation writes use `data_source_id`. + +### `2026-03-11` + +Breaking changes: + +- `PATCH /v1/blocks/{id}/children`: `after` replaced by `position`. +- `archived` removed/replaced by `in_trash`. +- `transcription` block type renamed to `meeting_notes`. + +## Pagination + +Response shape: + +```text +object: list +results: [...] +has_more: true|false +next_cursor: opaque string when has_more +``` + +Mechanics: + +- `GET` endpoints use query parameters: `start_cursor`, `page_size`. +- `POST` endpoints use JSON body fields: `start_cursor`, `page_size`. +- `page_size` max is generally 100. +- Cursors are opaque; pass `next_cursor` back as `start_cursor` verbatim. +- Do not persist assumptions about cursor format. + +Paginated surfaces include users, block children, comments, page property items, file uploads, data-source templates, views, view query results, data-source query, search, and custom emojis. + +## Rate limits and sizes + +Rate: + +- Average 3 requests/second per connection. +- Some bursts allowed. +- 429 responses include `Retry-After` integer seconds. Wait at least that long. +- Limits may change and may vary by workspace plan in the future. + +General request limits: + +- 500KB maximum payload. +- 1000 block elements maximum per request. +- Many arrays of block/rich-text objects cap at 100 elements. +- Append block children caps at 100 child blocks and two nesting levels per request. + +Value limits: + +- Rich text `text.content`: 2000 chars. +- Rich text link URL: 2000 chars. +- Equation expression: 1000 chars. +- URL: 2000 chars. +- Email/phone: 200 chars. +- Multi-select options: 100. +- Relation entries: 100. +- People entries: 100. + +## Error handling + +Error body has programmatic `code` plus human `message`; message text may change without a version bump. + +Important codes: + +- `400 invalid_json`: invalid body JSON. +- `400 invalid_request_url`: bad URL. +- `400 invalid_request`: unsupported request. +- `400 invalid_grant`: OAuth grant/refresh issue. +- `400 validation_error`: invalid payload/parameters. +- `400 missing_version`: missing `Notion-Version`. +- `401 unauthorized`: invalid bearer token. +- `403 restricted_resource`: missing permission/capability. +- `404 object_not_found`: missing or not shared with token owner/connection. +- `409 conflict_error`: data collision or temporary storage conflict; refresh inputs and retry cautiously. +- `429 rate_limited`: respect `Retry-After`. +- `500 internal_server_error`: Notion server error. +- `502 bad_gateway`: retry with backoff. +- `503 service_unavailable`: Notion unavailable or >60s timeout; retry later. +- `503 database_connection_unavailable`: database temporarily unavailable. +- `504 gateway_timeout`: retry with backoff. + +Retry policy: + +- Retry 429 after `Retry-After`. +- Retry 502/503/504 with exponential backoff and jitter. +- For data-source query 503, reduce `page_size` and narrow filters/sorts. +- Do not blind-retry creates after ambiguous failures; Notion does not document idempotency keys. + +## Endpoint map by owner + +### Pages + +- `POST /v1/pages` — create page. Parent can be page, data source, or workspace for public/PAT contexts. Body can use `markdown`, `children`/`content`, or `template` depending on mode. +- `GET /v1/pages/{page_id}` — page metadata/properties, not body blocks. +- `PATCH /v1/pages/{page_id}` — update properties/icon/cover/lock/template/content erase/trash via `in_trash`. +- `POST /v1/pages/{page_id}/move` — move regular page to page or data source parent. +- `GET /v1/pages/{page_id}/properties/{property_id}` — complete paginated property values. +- `GET /v1/pages/{page_id}/markdown` — agent-friendly page body markdown. +- `PATCH /v1/pages/{page_id}/markdown` — markdown exact updates or replace whole body. + +### Blocks + +- `GET /v1/blocks/{block_id}` — one block. +- `GET /v1/blocks/{block_id}/children` — first-level children only; recurse yourself. +- `PATCH /v1/blocks/{block_id}/children` — append children with `position`. +- `PATCH /v1/blocks/{block_id}` — update block fields or `in_trash`. +- `DELETE /v1/blocks/{block_id}` — trash block/page-block. +- `POST /v1/blocks/meeting_notes/query` — query meeting notes. + +### Data sources and databases + +- `GET /v1/databases/{database_id}` — retrieve database container and discover data-source IDs. +- `POST /v1/databases` — create database container and initial data source. +- `PATCH /v1/databases/{database_id}` — update container-level fields. +- `GET /v1/data_sources/{data_source_id}` — retrieve data source schema/table. +- `POST /v1/data_sources/{data_source_id}/query` — query rows/pages. +- `POST /v1/data_sources` — add a data source to an existing database. +- `PATCH /v1/data_sources/{data_source_id}` — update schema/source fields or move/trash data source. +- `GET /v1/data_sources/{data_source_id}/templates` — list templates. +- `POST /v1/databases/{database_id}/query` — legacy/deprecated database query endpoint. + +### Views + +- `GET /v1/views` — list views by database/data source. +- `POST /v1/views` — create view. +- `GET /v1/views/{view_id}` — retrieve view config. +- `PATCH /v1/views/{view_id}` — update view config. +- `DELETE /v1/views/{view_id}` — delete view. +- `POST /v1/views/{view_id}/queries` — create cached view query. +- `GET /v1/views/{view_id}/queries/{query_id}` — paginate cached query. +- `DELETE /v1/views/{view_id}/queries/{query_id}` — free cached query; idempotent. + +View query results expire after about 15 minutes. You cannot add extra filters/sorts to a view query; edit the view or use data-source query. + +### Comments + +- `GET /v1/comments?block_id=...` — list open comments on page/block; pages are blocks. +- `POST /v1/comments` — top-level page comment or reply to existing discussion. +- `GET /v1/comments/{comment_id}` — retrieve one comment. +- `PATCH /v1/comments/{comment_id}` — update own comment body. +- `DELETE /v1/comments/{comment_id}` — delete own comment. + +Comments can use `rich_text` or `markdown` body, mutually exclusive. + +### Search and users + +- `POST /v1/search` — title search over shared pages/data sources; not exhaustive inventory. +- `GET /v1/users` — paginated workspace users; requires user capability; PATs cannot list all users. +- `GET /v1/users/{user_id}` — retrieve a user/bot/guest in workspace. +- `GET /v1/users/me` — token's bot/current user. +- `GET /v1/custom_emojis` — paginated custom emoji list; supports exact `name` filter for name-to-ID lookup. + +Custom emoji/icon notes: + +- To set a custom emoji icon, use `type: "custom_emoji"` with the custom emoji `id`; do not infer IDs from markdown `:name:` syntax. Sources: official OpenAPI `customEmojiIconRequest` and `GET /v1/custom_emojis` in `https://developers.notion.com/openapi.json`. +- Native Notion icons are structured `type: "icon"` objects in current docs/SDK surfaces. Source: `https://developers.notion.com/page/changelog.md`. + +### OAuth + +- `POST /v1/oauth/token` — code exchange or refresh. +- `POST /v1/oauth/revoke` — revoke token. +- `POST /v1/oauth/introspect` — inspect token. + +OAuth token endpoints use Basic auth (`client_id:client_secret`) in addition to request body semantics. + +## Idempotency and concurrency + +No idempotency-key or optimistic-concurrency header is documented in the focused official corpus. + +Safe practices: + +- Store Notion IDs after creates. +- For ambiguous create failures, search/read by your own external key before retrying. +- Use exact `update_content` markdown matches when editing text so drift fails validation. +- Read after writes when a downstream action depends on new state. +- Use webhooks plus narrow filters for sync instead of full polling loops. diff --git a/skills/productivity/notion/references/api-knownness-packet.md b/skills/productivity/notion/references/api-knownness-packet.md new file mode 100644 index 0000000000000..bc73e3f60c3f5 --- /dev/null +++ b/skills/productivity/notion/references/api-knownness-packet.md @@ -0,0 +1,233 @@ +# Notion API Knownness Packet + +Generated from official Notion docs and specs on 2026-05-18. + +## Identity + +Provider: Notion + +API family: Notion REST API, Notion CLI (`ntn`), hosted Notion MCP, integration webhooks, Workers/Developer Platform, official JS/TS SDK. + +Protocol shape: REST JSON API plus multipart file upload endpoints, webhook event delivery, OAuth 2.0 public-connection auth, hosted MCP, and TypeScript Workers runtime. + +Primary version/date: REST `Notion-Version: 2026-03-11` for new code. SDK v5+ supports `2025-09-03` and `2026-03-11`; README default was `2025-09-03` in the 2026-05-18 fetch. + +Environments: production public API at `https://api.notion.com`; no sandbox found in public docs. Webhooks require public HTTPS receiver. Hosted MCP endpoint at `https://mcp.notion.com/mcp`. + +Access boundary: public docs/specs. No credentials used. No live workspace read/write probes run. + +Why Stockitup/Hermes needs it: future agents can correctly operate Notion pages/data sources/markdown/webhooks without stale database semantics, malformed curl snippets, or credential leakage. + +## Source Ranking + +Best machine-readable spec: + +- `https://developers.notion.com/openapi.json` +- Retrieved to `/tmp/notion-api-official-md/openapi.json`. +- OpenAPI 3.1.0; server `https://api.notion.com`; security schemes `bearerAuth` and `basicAuth`. +- Public documented paths observed: users, pages, page markdown, blocks, data sources, databases, search, comments, file uploads, custom emojis, views, meeting notes query, OAuth token/revoke/introspect. + +Lower-authority machine-readable lead: + +- `https://developers.notion.com/openapi-undocumented.json` +- Treat as unstable/private; do not build production integrations against it without explicit validation. + +Official docs/API reference: + +- `https://developers.notion.com/llms.txt` +- `https://developers.notion.com/llms-full.txt` +- `.md` docs under `developers.notion.com/reference`, `guides`, `cli`, and `page/changelog`. + +Official SDKs/repos: + +- JS/TS SDK: `https://github.com/makenotion/notion-sdk-js`, npm package `@notionhq/client`. +- CLI: `ntn`, docs at `https://developers.notion.com/cli/...`; source build path mentioned as `https://github.com/makenotion/cli.git`. +- Workers SDK / Agent SDK: product/dev docs mention `@notionhq/workers` and `@notionhq/agents-client`; treat as beta/alpha and re-check docs before production. + +Changelog/release notes: + +- `https://developers.notion.com/page/changelog.md` +- `https://developers.notion.com/reference/changes-by-version.md` + +Existing Stockitup evidence: + +- No actual Notion API integration found under `/home/snaz/stockitup` during the read-only scout. +- Only incidental/product-positioning Notion mentions were found. + +## Protocol Shape + +Base URL: + +```text +https://api.notion.com +``` + +API style: + +- REST resources under `/v1`. +- JSON bodies for normal endpoints. +- Multipart/form-data for `POST /v1/file_uploads/{file_upload_id}/send`. +- Webhooks are configured in the Developer Portal and delivered as POSTs to external HTTPS URLs. +- Hosted MCP is Streamable HTTP/SSE with OAuth-user auth. + +Versioning: + +- Required `Notion-Version` request header. +- Latest fetched docs recommend/support `2026-03-11` for current REST behavior. +- Breaking versions listed include `2026-03-11`, `2025-09-03`, and older versions. +- Additive changes can happen without version bumps; clients must tolerate unknown fields. + +## Auth and Trust + +Auth mechanisms: + +- Bearer token for REST API internal connections, PATs, and OAuth access tokens. +- OAuth 2.0 for public connections. +- Basic auth with client id/secret for OAuth token endpoint. +- HMAC-SHA256 webhook signature via `X-Notion-Signature` and subscription verification token. +- Hosted MCP OAuth-user auth; no bearer-token headless auth. + +OAuth details: + +- Auth URL uses `client_id`, `redirect_uri`, `response_type=code`, `owner=user`, optional `state`. +- Token exchange at `POST /v1/oauth/token`. +- Refresh returns new access and refresh tokens. +- Store `bot_id` as authorization key per docs. + +Secrets handling: + +- Hermes env: `NOTION_API_KEY`. +- Notion CLI env: `NOTION_API_TOKEN`. +- Tokens are opaque; new prefix `ntn_`, old `secret_` may work. +- Webhook verification tokens are secrets. + +## Resource Model + +Core objects: + +- Database: container object with data sources and container-level metadata/permissions. +- Data source: table/schema/row parent. Query/update schema here. +- Page: metadata/properties and parent; body content lives as blocks or markdown endpoint output. +- Block: tree node with type-specific object and optional children. +- View: data-source/database view config and cached query surface. +- Comment: page/block comment and discussion reply object. +- File Upload: upload lifecycle object, then reusable attachment ID. +- User/bot: current user/bot and workspace user data subject to capabilities. + +Important lifecycle/status facts: + +- Use `in_trash`, not `archived`, in `2026-03-11` payloads. +- File Upload status enum: `pending`, `uploaded`, `expired`, `failed`. +- Data-source query/view query pagination can cap around 10,000 results. +- File download URLs expire after about one hour. + +## Request Mechanics + +Pagination: cursor-based `has_more`, `next_cursor`, `start_cursor`, max `page_size` generally 100. + +Rate limits: average 3 requests/second per connection. Respect integer-second `Retry-After` on 429. + +Payload limits: 500KB overall, 1000 block elements, many arrays max 100, rich text 2000 chars. + +Retries: backoff 502/503/504; narrow data-source queries on 503; avoid unsafe create retries without dedupe. + +Idempotency: no idempotency-key documented. Store IDs, use external keys, and read-after-write. + +Search: eventually consistent and not exhaustive. Use known IDs/data-source query for inventory. + +File upload: create upload object, send multipart bytes, attach within one hour; no public delete/revoke API found. + +## Errors and Edge Cases + +Important status/code patterns: + +- `400 missing_version`, `validation_error`, `invalid_json`, `invalid_grant`. +- `401 unauthorized` for invalid token. +- `403 restricted_resource` for capability/permission issues. +- `404 object_not_found` for absent or not-shared objects. +- `409 conflict_error` for collision/storage conflict. +- `429 rate_limited` with `Retry-After`. +- `502`, `503`, `504` for transient server/upstream/timeouts. + +Open conflict list: + +- Some docs still say views unsupported; newer changelog/OpenAPI/views docs show views API. +- File upload guide says `archived` where object enum says `expired`. +- Multipart part count has 10,000 vs 1,000 doc/schema mismatch. +- MCP tool catalog differs between current tool page and changelog. + +## Webhooks / Events + +Subscription management: Developer Portal, public HTTPS URL, verification token flow. + +Signature: `X-Notion-Signature`, HMAC-SHA256 over raw body with verification token. + +Delivery: most within 1 minute, target within 5 minutes; not ordered; retries up to 8 attempts with final retry about 24 hours later. + +Event families: page, database, data source, comment; docs/changelog/index also mention file-upload/view/page-transcript events that need fresh validation before hardcoding. + +Receiver strategy: validate signature, enqueue/dedupe by event id, fetch latest state from REST API, handle out-of-order and aggregated events. + +## SDKs and Examples + +Official JS/TS SDK: + +- Package: `@notionhq/client`. +- Runtime: Node >=18. +- Current README supports `2025-09-03` and `2026-03-11`, defaulting to `2025-09-03`. +- Set `notionVersion: "2026-03-11"` explicitly for new code. +- Provides pagination helpers and typed error/code helpers. + +CLI: + +- `ntn api` for REST calls, `ntn files`, `ntn pages`, `ntn datasources`, `ntn workers`. +- `NOTION_API_TOKEN` overrides keychain auth. +- CLI can inspect endpoint docs/specs. + +MCP: + +- Hosted Notion MCP supports interactive AI clients with OAuth-user access. +- It does not currently support file uploads or bearer-token headless auth. + +## Stockitup Integration Map + +Existing locations checked: `/home/snaz/stockitup` broad Notion/API token/package searches. + +Existing code/specs found: none for real Notion API integration. + +Target owner if needed later: decide by product boundary. JAD server integration likely belongs under `packages/jad/server/integrations/notion/` for tenant/workspace use; central/control-plane use would belong in Buttler/future `butt`; C/Ring projection requires Stockitup Ring/API skills first. + +Deployment/ops concerns: server-side token storage only, no frontend tokens, explicit tenant/workspace/content sharing model, webhook receiver public HTTPS/HMAC verification, rate-limit queue. + +## Monitoring Candidate + +Should monitor: yes, if Stockitup/Hermes begins depending on Notion API behavior. + +Monitor sources: + +- `https://developers.notion.com/openapi.json` +- `https://developers.notion.com/llms.txt` +- `https://developers.notion.com/page/changelog.md` +- `https://developers.notion.com/reference/changes-by-version.md` +- `https://developers.notion.com/reference/request-limits.md` +- `https://developers.notion.com/reference/webhooks.md` +- `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` + +Expected noise: moderate. Changelog and OpenAPI may change often; monitor should digest diffs and only alert on API version, path/schema, auth, webhook, rate-limit, or SDK default changes. + +## Final Confidence + +Confidence: high for documented REST API core, auth, data-source split, pages/blocks/markdown/files, and request mechanics because official OpenAPI plus official markdown docs were fetched. + +Confidence: medium for webhooks/MCP/Workers details because docs show active drift and beta/alpha surfaces. + +Invalidators: + +- New Notion API version after `2026-03-11`. +- SDK default version changes. +- Webhook/MCP/Worker catalog changes. +- Production account behavior differing from public docs due plan/admin policy. + +Next useful step: + +- If Notion becomes operationally important, create a docs/spec monitor and run a credentialed read-only smoke test with an explicitly approved Notion workspace/token. diff --git a/skills/productivity/notion/references/block-types.md b/skills/productivity/notion/references/block-types.md index 943b6a4f999b9..24f0225f2c519 100644 --- a/skills/productivity/notion/references/block-types.md +++ b/skills/productivity/notion/references/block-types.md @@ -1,112 +1,252 @@ # Notion Block Types -Reference for creating and reading all common Notion block types via the API. +Reference for reading and writing common Notion blocks through the REST API. -## Creating blocks +Sources: -Use `PATCH /v1/blocks/{page_id}/children` with a `children` array. Each block follows this structure: +- `https://developers.notion.com/reference/block.md` +- `https://developers.notion.com/reference/get-block-children.md` +- `https://developers.notion.com/reference/patch-block-children.md` +- `https://developers.notion.com/reference/update-a-block.md` +- `https://developers.notion.com/guides/data-apis/working-with-page-content.md` + +## Block shape + +Every block has: ```json -{"object": "block", "type": "", "": { ... }} +{ + "object": "block", + "id": "...", + "parent": {"type": "page_id", "page_id": "..."}, + "type": "paragraph", + "has_children": false, + "in_trash": false, + "paragraph": {"rich_text": []} +} ``` -### Paragraph +The type-specific object is keyed by `type`. + +## Traversal + +- A page ID can be used as a `block_id`. +- `GET /v1/blocks/{block_id}/children` returns only first-level children. +- Recurse when `has_children` is true. +- Do not assume child blocks are returned inline with their parent. + +## Appending blocks + +Endpoint: + +```text +PATCH /v1/blocks/{block_id}/children +``` + +Use `position`, not legacy `after`: ```json -{"type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": "Hello world"}}]}} +{ + "position": {"type": "end"}, + "children": [ + {"object":"block","type":"paragraph","paragraph":{"rich_text":[{"text":{"content":"Hello"}}]}} + ] +} ``` -### Headings +Position variants: ```json -{"type": "heading_1", "heading_1": {"rich_text": [{"text": {"content": "Title"}}]}} -{"type": "heading_2", "heading_2": {"rich_text": [{"text": {"content": "Section"}}]}} -{"type": "heading_3", "heading_3": {"rich_text": [{"text": {"content": "Subsection"}}]}} +{"type":"end"} +{"type":"start"} +{"type":"after_block","after_block_id":"..."} ``` -### Bulleted list +Limits: + +- Max 100 child blocks per request. +- Max two nesting levels in one request. +- Existing blocks cannot be moved with append; use page move for pages where applicable. + +## Common block creation snippets + +Paragraph: ```json -{"type": "bulleted_list_item", "bulleted_list_item": {"rich_text": [{"text": {"content": "Item"}}]}} +{"object":"block","type":"paragraph","paragraph":{"rich_text":[{"text":{"content":"Hello world"}}]}} ``` -### Numbered list +Headings: ```json -{"type": "numbered_list_item", "numbered_list_item": {"rich_text": [{"text": {"content": "Step 1"}}]}} +{"object":"block","type":"heading_1","heading_1":{"rich_text":[{"text":{"content":"Title"}}]}} +{"object":"block","type":"heading_2","heading_2":{"rich_text":[{"text":{"content":"Section"}}]}} +{"object":"block","type":"heading_3","heading_3":{"rich_text":[{"text":{"content":"Subsection"}}]}} +{"object":"block","type":"heading_4","heading_4":{"rich_text":[{"text":{"content":"Detail"}}]}} ``` -### To-do / checkbox +Bulleted and numbered list items: ```json -{"type": "to_do", "to_do": {"rich_text": [{"text": {"content": "Task"}}], "checked": false}} +{"object":"block","type":"bulleted_list_item","bulleted_list_item":{"rich_text":[{"text":{"content":"Item"}}]}} +{"object":"block","type":"numbered_list_item","numbered_list_item":{"rich_text":[{"text":{"content":"Step"}}]}} ``` -### Quote +To-do: ```json -{"type": "quote", "quote": {"rich_text": [{"text": {"content": "Something wise"}}]}} +{"object":"block","type":"to_do","to_do":{"rich_text":[{"text":{"content":"Task"}}],"checked":false}} ``` -### Callout +Toggle: ```json -{"type": "callout", "callout": {"rich_text": [{"text": {"content": "Important note"}}], "icon": {"emoji": "💡"}}} +{"object":"block","type":"toggle","toggle":{"rich_text":[{"text":{"content":"Open"}}],"children":[]}} ``` -### Code +Quote: ```json -{"type": "code", "code": {"rich_text": [{"text": {"content": "print('hello')"}}], "language": "python"}} +{"object":"block","type":"quote","quote":{"rich_text":[{"text":{"content":"Something wise"}}]}} ``` -### Toggle +Callout: ```json -{"type": "toggle", "toggle": {"rich_text": [{"text": {"content": "Click to expand"}}]}} +{"object":"block","type":"callout","callout":{"rich_text":[{"text":{"content":"Important"}}],"icon":{"type":"emoji","emoji":"💡"},"color":"blue_background"}} ``` -### Divider +Code: ```json -{"type": "divider", "divider": {}} +{"object":"block","type":"code","code":{"rich_text":[{"text":{"content":"print('hello')"}}],"language":"python"}} ``` -### Bookmark +Divider: ```json -{"type": "bookmark", "bookmark": {"url": "https://example.com"}} +{"object":"block","type":"divider","divider":{}} ``` -### Image (external URL) +Bookmark: ```json -{"type": "image", "image": {"type": "external", "external": {"url": "https://example.com/photo.png"}}} +{"object":"block","type":"bookmark","bookmark":{"url":"https://example.com","caption":[]}} ``` -## Reading blocks +Image external URL: + +```json +{"object":"block","type":"image","image":{"type":"external","external":{"url":"https://example.com/photo.png"},"caption":[]}} +``` + +Image uploaded with File Upload API: + +```json +{"object":"block","type":"image","image":{"type":"file_upload","file_upload":{"id":"FILE_UPLOAD_ID"},"caption":[]}} +``` + +Read-only/generated media caveats: + +- `link_preview` blocks can be returned by the API but cannot be created or appended through the public block API; use bookmarks or rich-text links for API-created content. Source: `https://developers.notion.com/reference/block.md`. +- Rich-text `template_mention` values such as `today`, `now`, and `me` are populated template placeholders, not generic programmatic mention primitives. Source: `https://developers.notion.com/reference/rich-text.md`. + +Child page: + +```json +{"object":"block","type":"child_page","child_page":{"title":"Child title"}} +``` -When reading blocks from `GET /v1/blocks/{page_id}/children`, each block has a `type` field. Extract readable text like this: +Table of contents: + +```json +{"object":"block","type":"table_of_contents","table_of_contents":{"color":"default"}} +``` + +## Child-capable block types + +Common child-capable types include: + +```text +paragraph +quote +callout +toggle +bulleted_list_item +numbered_list_item +to_do +synced_block +column +column_list +table +table_row +child_page +child_database +meeting_notes +``` + +Always confirm current child support from the block reference before relying on less common types. + +Meeting notes: + +- `meeting_notes` replaced the older `transcription` block name in `2026-03-11` responses, but meeting-notes blocks are read-only; the API cannot create or update them. Source: `https://developers.notion.com/reference/block.md`. +- Query meeting notes with `POST /v1/blocks/meeting_notes/query` or retrieve existing meeting-notes blocks, then fetch `summary_block_id`, `notes_block_id`, and `transcript_block_id` children as needed. Sources: `https://developers.notion.com/reference/query-meeting-notes.md` and official OpenAPI operation `query-meeting-notes`. +- For page markdown reads, use `include_transcript=true` when transcript text is needed. + +## Reading text + +For rich-text blocks, concatenate `.plain_text` from the relevant rich-text array: + +```text +paragraph.rich_text +heading_1.rich_text +heading_2.rich_text +heading_3.rich_text +heading_4.rich_text +bulleted_list_item.rich_text +numbered_list_item.rich_text +to_do.rich_text +toggle.rich_text +quote.rich_text +callout.rich_text +code.rich_text +``` + +Media blocks usually carry caption rich text plus one of: + +```text +file.url + expiry_time +external.url +file_upload.id +``` + +## Updating and deleting + +Update one block: + +```text +PATCH /v1/blocks/{block_id} +``` + +Rules: + +- Included fields replace the entire field value. +- Omitted fields remain unchanged. +- Children are not updated by parent update. +- Update page/database-specific fields with page/database/data-source endpoints, not block update. + +Delete/trash one block: + +```text +DELETE /v1/blocks/{block_id} +``` -| Type | Text location | Extra fields | -|------|--------------|--------------| -| `paragraph` | `.paragraph.rich_text` | — | -| `heading_1/2/3` | `.heading_N.rich_text` | — | -| `bulleted_list_item` | `.bulleted_list_item.rich_text` | — | -| `numbered_list_item` | `.numbered_list_item.rich_text` | — | -| `to_do` | `.to_do.rich_text` | `.to_do.checked` (bool) | -| `toggle` | `.toggle.rich_text` | has children | -| `code` | `.code.rich_text` | `.code.language` | -| `quote` | `.quote.rich_text` | — | -| `callout` | `.callout.rich_text` | `.callout.icon.emoji` | -| `divider` | — | — | -| `image` | `.image.caption` | `.image.file.url` or `.image.external.url` | -| `bookmark` | `.bookmark.caption` | `.bookmark.url` | -| `child_page` | — | `.child_page.title` | -| `child_database` | — | `.child_database.title` | +or patch `in_trash` where supported. In `2026-03-11`, use `in_trash`, not `archived`. -Rich text arrays contain objects with `.plain_text` — concatenate them for readable output. +## Unsupported and unknown blocks ---- +Notion can return unsupported block types as `unsupported`, and markdown export can return `` placeholders. For unknown markdown blocks: -*Contributed by [@dogiladeveloper](https://github.com/dogiladeveloper)* +- try structured block retrieval by ID; +- confirm the connection has access; +- expect some UI block types to be partially or not supported by public API. diff --git a/skills/productivity/notion/references/data-sources-and-pages.md b/skills/productivity/notion/references/data-sources-and-pages.md new file mode 100644 index 0000000000000..d11b6672adf39 --- /dev/null +++ b/skills/productivity/notion/references/data-sources-and-pages.md @@ -0,0 +1,301 @@ +# Data Sources, Databases, Pages, Blocks, Properties, and Views + +Sources: + +- `https://developers.notion.com/reference/database.md` +- `https://developers.notion.com/reference/data-source.md` +- `https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03.md` +- `https://developers.notion.com/guides/data-apis/working-with-databases.md` +- `https://developers.notion.com/guides/data-apis/working-with-page-content.md` +- `https://developers.notion.com/guides/data-apis/working-with-views.md` +- public OpenAPI spec + +## Resource graph + +Current model: + +```text +database container + └─ data_source table/schema/row parent + ├─ page rows with properties + └─ view definitions / queries +page body + └─ block children tree +``` + +Database: + +- `object: "database"`. +- Container that can have one or more data sources. +- Carries container fields: title, description, parent, icon, cover, URL/public URL, `is_inline`, `in_trash`, timestamps/users, `data_sources:[{id,name}]`. +- Permissions are managed on the database/container, not individual data sources. + +Data source: + +- `object: "data_source"`. +- Represents a table/schema/row collection. +- Carries `properties` schema. +- Parent is usually a `database_id`. +- Pages under a data source are rows. + +## Migration rules from old database API + +Old work often says “database” when it now means “data source.” Correct it by endpoint shape: + +- Old retrieve schema: `GET /v1/databases/{database_id}`. + - New: retrieve database to discover `data_sources`; call `GET /v1/data_sources/{data_source_id}` for schema. +- Old query rows: `POST /v1/databases/{database_id}/query`. + - New: `POST /v1/data_sources/{data_source_id}/query`. +- Old create row parent: `parent: { database_id: ... }`. + - New: `parent: { type: "data_source_id", data_source_id: ... }`. +- Old relations targeting database ID in writes. + - New: relation schema/write targets use `data_source_id`. +- Rich-text database mentions remain database IDs, not data-source IDs. +- Old search result/filter value `database`. + - New: `data_source`. + +Database IDs and data-source IDs are not interchangeable. + +## Page model + +Page object fields include: + +- `object: "page"` +- `id` +- created/edited timestamps/users +- `parent` +- `properties` +- `icon`, `cover` +- `url`, `public_url` +- `in_trash` +- optional lock fields in current OpenAPI responses + +Parent behavior: + +- Parent page/workspace: only title property is valid. +- Parent data source: properties must match the data-source schema. + +Body content: + +- Retrieve page returns metadata/properties, not body blocks. +- Use page ID as `block_id` with block-children endpoint for structured traversal. +- Use markdown endpoint for agent-readable body text. + +Property completeness: + +- Retrieve-page can truncate `people`, `relation`, `rich_text`, and `title` values around 25 refs. +- Use `GET /v1/pages/{page_id}/properties/{property_id}` for complete paginated values. + +## Data-source query + +Endpoint: + +```text +POST /v1/data_sources/{data_source_id}/query +``` + +Common body fields: + +- `filter` +- `sorts` +- `start_cursor` +- `page_size` +- `filter_properties` +- `in_trash` +- `result_type`, useful for wiki-like cases + +Operational notes: + +- Use filters and narrow `page_size` for large sources. +- Query can stop/cap around 10,000 results. For large syncs, use filters and webhooks instead of full polling. +- Always inspect query `request_status`; a response can be a valid page of results but still marked incomplete because the query-result limit was reached. +- `filter_properties[]` is a URL/query parameter on a `POST` endpoint, not a JSON body field in raw HTTP; SDKs may expose it as a named option. +- Query 503 guidance recommends backoff with jitter, smaller page size, and narrower filters/sorts. + +## Property schemas and values + +Data-source property object = schema/column definition: + +- `id`, `name`, `description`, `type`, plus a type-specific config object. + +Common schema property types: + +```text +checkbox +created_by +created_time +date +email +files +formula +last_edited_by +last_edited_time +multi_select +number +people +phone_number +place +relation +rich_text +rollup +select +status +title +unique_id +url +``` + +Page property value object = row/page value: + +- `id`, `type`, and a type-specific value. +- Some values are generated/read-only. +- Page property item endpoint returns either one value or a paginated list of property items, depending on property type. + +Relation schema: + +- Use `data_source_id` for write targets after `2025-09-03`. +- Related data source/database must usually be shared with the connection. + +## Blocks and traversal + +Block object fields: + +- `object: "block"` +- `id` +- `parent` +- `type` +- created/edited timestamps/users +- `has_children` +- `in_trash` +- type-specific object keyed by `type` + +Traversal: + +1. Use page ID as block ID. +2. Call `GET /v1/blocks/{block_id}/children`. +3. For each block with `has_children: true`, recurse. +4. Preserve sibling order from the API response, but do not assume unrelated list endpoint ordering unless documented. + +Appending: + +- `PATCH /v1/blocks/{block_id}/children` appends/creates child blocks. +- Use `position` with `end`, `start`, or `after_block`. +- Existing blocks cannot be moved with append endpoint. +- Max 100 child blocks per request. +- Max two nesting levels in one request. + +Updating/deleting: + +- `PATCH /v1/blocks/{block_id}` replaces included block fields; omitted fields unchanged. +- `DELETE /v1/blocks/{block_id}` sets `in_trash: true`. +- Children are not updated by updating a parent block. + +## Views + +Views API exists in the 2026 docs/OpenAPI despite stale older FAQ text. + +View object fields include: + +- `object: "view"` +- `id` +- parent database +- `data_source_id` (null for dashboard) +- `name` +- `type` +- `filter` +- `sorts` +- `quick_filters` +- `configuration` +- timestamps/users +- URL + +View types: + +```text +table +board +list +calendar +timeline +gallery +form +chart +map +dashboard +``` + +View endpoints: + +- `GET /v1/views?database_id=...` — list views for database. +- `GET /v1/views?data_source_id=...` — list views over data source. +- `GET /v1/views/{view_id}` — full config. +- `POST /v1/views` — create view. +- `PATCH /v1/views/{view_id}` — update view. +- `DELETE /v1/views/{view_id}` — delete view; cannot delete last remaining database view. +- `POST /v1/views/{view_id}/queries` — create cached query from view filters/sorts. +- `GET /v1/views/{view_id}/queries/{query_id}` — paginate cached query. +- `DELETE /v1/views/{view_id}/queries/{query_id}` — free cached query. + +View query notes: + +- Cached query expires after about 15 minutes. +- You cannot add extra filters/sorts to a view query. +- Use data-source query if you need ad-hoc filters/sorts. + +Create/update/dashboard caveats: + +- Views require API version `2025-09-03` or newer. Source: `https://developers.notion.com/guides/data-apis/working-with-views.md`. +- `POST /v1/views` requires `data_source_id` and exactly one placement parent: `database_id`, dashboard `view_id`, or `create_database`. Database IDs and data-source IDs are different. Source: `https://developers.notion.com/reference/create-view.md`. +- Dashboard view `configuration.rows` is read-only; manage dashboard layout by creating/deleting widget views. Widgets can only be placed with `view_id`, and the docs cap widgets at four per row. Source: `https://developers.notion.com/guides/data-apis/working-with-views.md`. +- `PATCH /v1/views/{view_id}` updates configuration by shallow merge and still needs required config fields. Source: `https://developers.notion.com/reference/update-view.md`. +- View-query access still depends on data-source/database access; `404 object_not_found` can mean the connection cannot access the underlying database. Source: `https://developers.notion.com/reference/get-view-query-results.md`. + +## Search + +Endpoint: + +```text +POST /v1/search +``` + +Use for title discovery across shared pages/data sources. Do not use as authoritative inventory. + +Filters: + +- `filter.property: "object"` +- `filter.value: "page"` or `"data_source"` + +Limitations: + +- Not exhaustive. +- Indexing after sharing/OAuth is not immediate. +- Results can change while paginating. +- Use data-source query to filter within one data source. + +## Comments + +Pages are blocks for comment-listing purposes. + +Endpoints: + +- `GET /v1/comments?block_id=...` — list open comments. +- `POST /v1/comments` — add page comment or reply to discussion. +- `GET /v1/comments/{comment_id}` — retrieve. +- `PATCH /v1/comments/{comment_id}` — update. +- `DELETE /v1/comments/{comment_id}` — delete. + +Limits: + +- REST API cannot start a new inline discussion on an arbitrary text range. +- It can reply to existing discussions via `discussion_id`. +- It cannot retrieve resolved comments. +- Connections can only delete/update comments they created. + +## Common mistakes + +- Creating data-source rows with `database_id` parent in new code. +- Querying `/databases/{id}/query` for new work. +- Treating retrieve page as page-body retrieval. +- Not recursing `has_children` blocks. +- Forgetting page-property endpoint for complete relation/people/title/rich-text values. +- Assuming views are unsupported because older 2025 text says so. diff --git a/skills/productivity/notion/references/deep-edge-cases-and-codegen.md b/skills/productivity/notion/references/deep-edge-cases-and-codegen.md new file mode 100644 index 0000000000000..31b5855e6315f --- /dev/null +++ b/skills/productivity/notion/references/deep-edge-cases-and-codegen.md @@ -0,0 +1,144 @@ +# Notion Deep Edge Cases, Drift, and Codegen Notes — 2026-05-18 + +Generated/synthesized from official Notion sources and the public OpenAPI on `2026-05-18T04:46:46Z`. + +Primary source anchors: + +- `https://developers.notion.com/openapi.json` — SHA-256 `c781691e6316b679648c83ff8f18a9dd70943fa24dbe35be69d19ff1cb274174` at this fetch. +- `https://developers.notion.com/llms.txt` — official docs link index. +- `https://developers.notion.com/page/changelog.md` +- `https://developers.notion.com/reference/changes-by-version.md` +- `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` + +## Problem this reference solves + +The first Notion pass taught the big model: `2026-03-11`, data sources, markdown, files, webhooks, MCP, and SDK defaults. This second pass records the edge cases that break integrations and typed clients: stale operation IDs, official-doc contradictions, view/query semantics, OAuth lifecycle traps, property-value incompleteness, webhook version drift, and package/tool-surface drift. + +## Claim matrix + +### Official spec says one thing; operation IDs sometimes say another + +- Source: `openapi.json` paths under `/v1/data_sources`. +- Direct fact: `POST /v1/data_sources/{data_source_id}/query` still has operationId `post-database-query`; `POST /v1/data_sources` still has operationId `create-a-database`. +- Future behavior: generated clients should key canonical routing on HTTP method + path + current docs, then optionally override stale operation IDs. + +### Views are first-class current API, despite stale older prose + +- Sources: `openapi.json` `/v1/views*`, `https://developers.notion.com/guides/data-apis/working-with-views.md`, `https://developers.notion.com/reference/view.md`. +- Direct fact: views expose list/create/retrieve/update/delete plus cached query create/results/delete. +- Important semantics: view query creates a cached result set, expires after about 15 minutes, cannot accept ad-hoc filters/sorts, and shares the 10k-ish query-depth limit. +- Future behavior: use views when a saved Notion view is the semantic truth; use data-source query when Hermes owns ad-hoc filters/sorts. + +### Position objects differ by endpoint + +- Sources: `patch-block-children`, page create/update markdown docs, views create docs, OpenAPI schemas. +- Direct fact: block append `position` supports `start`, `end`, and `after_block`; create-page and view placement use related but not identical variants (`page_start`/`page_end`, `after_view`, dashboard/widget placement, and `create_database.position`). +- Future behavior: do not reuse one generic `Position` type blindly across pages, blocks, markdown, and views. + +### `in_trash` wins for REST, but schema/prose drift still exists + +- Sources: `2026-03-11` upgrade guide, trash-page docs, OpenAPI page schemas. +- Direct fact: current migration docs say `archived` was removed/replaced by `in_trash`; official schemas still expose archive-ish names in some places (`is_archived` observed in scout output). +- Future behavior: write `in_trash` for REST; tolerate old/read-only archive-ish fields in responses/webhooks where docs require compatibility. + +### Page templates are asynchronous and can erase content + +- Sources: page create/update docs, data source templates endpoint. +- Direct fact: `GET /v1/data_sources/{data_source_id}/templates` lists template pages; page create/update can apply default or specific template; template application is async; update can use `erase_content`. +- Future behavior: after applying a template, read back or wait for webhooks before depending on content; treat `erase_content` as destructive. + +### Page property values are not complete in retrieve-page responses + +- Sources: page-property-values and property-item docs. +- Direct fact: relation/people/rich_text/title references can truncate around 25 refs in `GET /v1/pages/{page_id}`. +- Future behavior: for correctness, call `GET /v1/pages/{page_id}/properties/{property_id}` and paginate property items; final rollup values may only be reliable after `has_more: false`. + +### Data-source query can be successful but incomplete + +- Sources: data-source query docs, request-limits docs, views docs. +- Direct fact: data-source and view queries can cap around 10,000 matching results and expose `request_status.type == "incomplete"` with reason like `query_result_limit_reached`. +- Future behavior: sync code must inspect `request_status`; use filters/sharding/webhooks instead of treating `has_more: false` alone as complete inventory. + +### Property/schema enum coverage differs between prose docs and OpenAPI + +- Sources: property object docs, page property values docs, OpenAPI component schemas. +- Direct fact: OpenAPI exposes variants and request/response shapes that prose docs may not emphasize (`button`, `location`, `last_visited_time`, `place`, `verification`); `place` may read as null/unsupported; `verification` is wiki-specific. +- Future behavior: implement discriminated unions with unknown fallback and nullable unsupported values; prefer stable property IDs over names. + +### OAuth refresh rotates both secrets; redirect URI has a conditional rule + +- Sources: public connections guide and OAuth token/refresh docs. +- Direct facts: refreshing returns a new access token and refresh token; token exchange `redirect_uri` is required if used at authorization or if multiple redirect URIs exist, and disallowed when exactly one redirect URI exists and the auth URL omitted it. +- Future behavior: update access+refresh atomically; store `bot_id` as authorization key; do not assume `expires_in` exists; treat `refresh_token` as single-owner. + +### PATs are useful but operationally brittle + +- Sources: personal access token docs, API key handling docs. +- Direct facts: PATs expire after one year; guests/restricted members cannot create them; admin policy/revocation can invalidate API access; PATs cannot list all workspace users. +- Future behavior: use PATs for trusted personal scripts/CLI only; use public OAuth or internal connections for products. + +### Webhook signatures need raw-body verification and replay/dedupe + +- Sources: webhook reference and event delivery docs. +- Direct facts: `X-Notion-Signature` is `sha256=` HMAC over the request body with `verification_token`; no signature timestamp/nonce is documented; delivery can retry and arrive out of order. +- Future behavior: verify raw bytes before parsing, compare timing-safely with length check, dedupe by event `id`, order by timestamp only as a hint, then fetch latest REST state. + +### Webhook versioning is not the REST header + +- Sources: webhook docs and upgrade guides. +- Direct facts: webhook subscriptions carry a Developer Portal API version; 2025 changed data-source event shapes; 2026 webhook payloads are documented as identical to 2025 even though REST renamed `archived` to `in_trash`. +- Future behavior: version webhook parsers separately from REST clients and preserve old webhook payload fields if docs say they remain. + +### File uploads are a three-mode lifecycle, not one upload URL + +- Sources: file upload reference/guides. +- Direct facts: modes are `single_part`, `multi_part`, and `external_url`; multi-part uses `/send` parts then `/complete`; external import is async; attach only when `status == uploaded`. +- Future behavior: model File Upload as a lifecycle object; no public delete/revoke API found; do not cache signed download URLs. + +### MCP/Workers/CLI are moving surfaces + +- Sources: Notion MCP docs/well-known metadata, `ntn` docs/npm, Workers docs/npm, SDK repo/npm. +- Direct facts from second pass package/source scouts: hosted MCP is OAuth-user and no headless bearer auth; file uploads are not hosted-MCP-supported; `@notionhq/client` latest observed `5.21.0` but default API version remains `2025-09-03`; `ntn` latest observed `0.14.0`; `@notionhq/workers` latest observed `0.4.0` and requires Node 22/npm 10; local `@notionhq/notion-mcp-server` package and repo/release versions can drift. +- Future behavior: treat non-REST developer-platform surfaces as monitor-first/beta-ish; re-check docs/package metadata before production implementation. + +## Codegen rules + +1. Pin request header `Notion-Version: 2026-03-11` for new generated clients. +2. Generate path/method APIs from OpenAPI, but override stale operation IDs that still say database for data-source paths. +3. Keep `object`/`type` discriminators, but add unknown fallbacks for future resource/property/block/view types. +4. Model every list as cursor-paginated unless proven otherwise; treat cursors as opaque. +5. Model incompleteness separately from pagination: `request_status`, markdown `truncated`, and `unknown_block_ids` are correctness signals. +6. Split similar-but-different position types by endpoint family. +7. Use stable property IDs internally; allow names only at UX/boundary level. +8. Make create/retry paths dedupe-aware because Notion documents no idempotency key. +9. Treat OAuth token storage as an atomic refresh-token-family update, not a stateless bearer refresh. +10. Keep REST, webhook, MCP, Workers, CLI, and SDK versions as separate monitored surfaces. + +## Monitor surface snapshot + +Official/no-credential inputs suitable for a quiet monitor: + +- OpenAPI: `https://developers.notion.com/openapi.json` +- Docs index: `https://developers.notion.com/llms.txt` +- Changelog: `https://developers.notion.com/page/changelog.md` +- Version changes: `https://developers.notion.com/reference/changes-by-version.md` +- SDK README: `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` +- NPM packages: `@notionhq/client`, `ntn`, `@notionhq/workers`, `@notionhq/notion-mcp-server` +- MCP discovery: `https://www.notion.com/.well-known/mcp.json`, `https://mcp.notion.com/.well-known/oauth-authorization-server` + +Use `scripts/notion_api_surface_snapshot.py` to produce a JSON snapshot for diffing. Do not schedule chat-alerting cron until Notion is operationally important enough to warrant noise. + +## Observed package/source metadata in this pass + +- `@notionhq/client`: latest `5.21.0`; source `https://registry.npmjs.org/@notionhq%2fclient` +- `ntn`: latest `0.14.0`; source `https://registry.npmjs.org/ntn` +- `@notionhq/workers`: latest `0.4.0`; source `https://registry.npmjs.org/@notionhq%2fworkers` +- `@notionhq/notion-mcp-server`: latest `2.2.1`; source `https://registry.npmjs.org/@notionhq%2fnotion-mcp-server` + +Extra source hashes: + +- `llms.txt`: SHA-256 `76afca5f0a061e72ee5da364a03660860b4df910ca8e35ada074c9f752d2efde`, bytes `27627`, URL `https://developers.notion.com/llms.txt` +- `changelog.md`: SHA-256 `321d9c59fede7eabce45fd4ebe90af3cc7254ace1f657807047a735b8978f197`, bytes `32532`, URL `https://developers.notion.com/page/changelog.md` +- `changes-by-version.md`: SHA-256 `22f6573f8e3f8121bbe2f717c6cdbbf5fd727cdbec259a0c45c9edb4af24adcd`, bytes `4990`, URL `https://developers.notion.com/reference/changes-by-version.md` +- `sdk-readme`: SHA-256 `a4941bcdcddfd114c298c3d112e3fce0b39debb4dc353312ab26731f06eb4d7d`, bytes `17034`, URL `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` +- `mcp-well-known`: SHA-256 `6976115d9b63114c1e89c999a35702ef84e3ce578862bf1d201ae9dab10bafb9`, bytes `237`, URL `https://www.notion.com/.well-known/mcp.json` diff --git a/skills/productivity/notion/references/file-uploads.md b/skills/productivity/notion/references/file-uploads.md new file mode 100644 index 0000000000000..9d0d27d31ada2 --- /dev/null +++ b/skills/productivity/notion/references/file-uploads.md @@ -0,0 +1,163 @@ +# Notion File Uploads + +Sources: + +- `https://developers.notion.com/reference/file-object.md` +- `https://developers.notion.com/reference/file-upload.md` +- `https://developers.notion.com/reference/create-file.md` +- `https://developers.notion.com/reference/upload-file.md` +- `https://developers.notion.com/reference/complete-file-upload.md` +- `https://developers.notion.com/reference/list-file-uploads.md` +- `https://developers.notion.com/reference/retrieve-file-upload.md` +- `https://developers.notion.com/guides/data-apis/working-with-files-and-media.md` +- `https://developers.notion.com/cli/guides/file-uploads.md` + +## File object types + +`file`: + +- Notion-hosted file already attached in workspace. +- Response includes signed URL and `expiry_time`. +- URL expires after about one hour; re-fetch object for a fresh URL. + +`external`: + +- Stable public HTTPS URL hosted outside Notion. +- No Notion file storage or URL expiry. + +`file_upload`: + +- File uploaded through File Upload API. +- Reference by File Upload ID after status is `uploaded`. +- Attach to page/block/page icon/page cover/database files property. + +## CLI fast path + +Use `ntn` if available: + +```bash +ntn files create < ./photo.png +ntn files create --external-url https://example.com/file.pdf --filename file.pdf +ntn files get FILE_UPLOAD_ID +ntn files list +``` + +For scripts, `ntn files create --plain` prints tab-separated output with ID first. `--json` exists but prefer structured text unless an existing program requires JSON. + +## HTTP direct upload: <=20 MB + +### 1. Create upload object + +```bash +curl -sS -X POST "https://api.notion.com/v1/file_uploads" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"filename":"photo.png","content_type":"image/png"}' +``` + +Response includes `id`, status `pending`, and `upload_url`. + +### 2. Send bytes + +This endpoint uses multipart/form-data field `file`: + +```bash +curl -sS -X POST "https://api.notion.com/v1/file_uploads/${FILE_UPLOAD_ID}/send" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -F "file=@photo.png" +``` + +Do not use a PUT to a presigned URL; current official docs use `POST /send` multipart. + +### 3. Attach the file + +Example image block: + +```json +{ + "object": "block", + "type": "image", + "image": { + "type": "file_upload", + "file_upload": {"id": "FILE_UPLOAD_ID"}, + "caption": [] + } +} +``` + +Attach within one hour of creation/upload. Once attached successfully, the upload becomes permanent/reusable in the workspace and no longer has an expiry time. + +## Multi-part upload: >20 MB + +Use when file is over 20 MiB and workspace plan supports the size. + +1. Create File Upload with `mode: "multi_part"`, `number_of_parts`, filename/content type. +2. Split into 5-20 MiB parts; final part may be under 5 MiB. Docs recommend 10 MiB parts. +3. Send each part to `/send` with form fields `file` and `part_number`. +4. Complete with `POST /v1/file_uploads/{file_upload_id}/complete`. +5. Attach within one hour. + +Parts may be sent concurrently/out of order, but rate limits still apply and completion validates full size. + +Doc conflict to watch: create schema allows `number_of_parts` up to 10,000, while send schema lists `part_number` max 1,000. Test current behavior before designing >1,000-part uploads. + +## External URL import + +Create with: + +```json +{ + "mode": "external_url", + "external_url": "https://example.com/file.pdf", + "filename": "file.pdf" +} +``` + +Rules: + +- URL must be public HTTPS. +- Server should expose `Content-Type` and `Content-Length` for Notion validation. +- Import is asynchronous; poll `GET /v1/file_uploads/{id}` or listen to file-upload webhooks if available. +- Final status is `uploaded` or `failed`. +- Failed imports cannot be attached or reused; create a new upload. + +## Size and lifecycle limits + +- Free workspace file limit: 5 MiB. +- Paid workspace file limit: 5 GiB. +- Direct single-part upload: <=20 MiB. +- Larger files use multi-part. +- Filename max: 900 bytes including extension. +- Status enum: `pending`, `uploaded`, `expired`, `failed`. +- Unattached uploads expire after about one hour. +- There is no public API to delete/revoke a created File Upload. +- Signed download URLs expire after about one hour. + +Bot user response can expose `workspace_limits.max_file_upload_size_in_bytes`; use it to avoid predictable validation errors. + +## Attach targets + +Uploaded files can be attached to: + +- media blocks: file, image, pdf, audio, video; +- data-source/page `files` properties; +- page icon; +- page cover. + +The file type must match context. For example: no PDF as page icon, no video file in image block. + +## Common errors + +- `400 validation_error`: content too large, invalid MIME/extension, invalid filename, expired/pending state mismatch. +- `409 conflict_error`: rare third-party storage downtime while sending contents; retry later. +- `404 object_not_found`: upload ID not found for token/connection. +- `429 rate_limited`: back off per `Retry-After`. + +## Operational pitfalls + +- Do not cache signed file URLs. +- Do not retry upload creates after ambiguous failure without dedupe; new File Upload IDs may be created. +- If uploading large files concurrently, throttle to Notion's 3 req/s average limit. +- External imports depend on third-party server HEAD/GET behavior; failure may not be Notion's fault. diff --git a/skills/productivity/notion/references/markdown-workflows.md b/skills/productivity/notion/references/markdown-workflows.md new file mode 100644 index 0000000000000..02ce2d29f719a --- /dev/null +++ b/skills/productivity/notion/references/markdown-workflows.md @@ -0,0 +1,241 @@ +# Notion Markdown Workflows + +Sources: + +- `https://developers.notion.com/guides/data-apis/working-with-markdown-content.md` +- `https://developers.notion.com/guides/data-apis/enhanced-markdown.md` +- `https://developers.notion.com/reference/retrieve-page-markdown.md` +- `https://developers.notion.com/reference/update-page-markdown.md` +- `https://developers.notion.com/reference/post-page.md` + +## Surfaces + +Create page from markdown: + +```text +POST /v1/pages +``` + +Read page markdown: + +```text +GET /v1/pages/{page_id}/markdown +``` + +Update page markdown: + +```text +PATCH /v1/pages/{page_id}/markdown +``` + +Markdown is often better than block JSON for agents. Use block APIs when you need exact structure, unsupported block details, or precise media/object manipulation. + +## Create page from markdown + +Body shape: + +```json +{ + "parent": {"type": "page_id", "page_id": "..."}, + "markdown": "# Title\n\nBody" +} +``` + +Rules: + +- `markdown` is mutually exclusive with `content`/`children`. +- `template` cannot be combined with `children`. +- If title property is omitted, Notion can derive title from first H1. +- Under a data source, properties must match schema. + +Shell quoting pitfall: send JSON with escaped newlines (`\n`). Do not let the shell convert them to literal newlines inside JSON. + +## Read page markdown + +Endpoint response includes a `page_markdown` object with: + +- `object` +- `id` +- `markdown` +- `truncated` +- `unknown_block_ids` + +Query parameter: + +- `include_transcript=true|false` for meeting note transcripts; default false. + +Unknown blocks appear when: + +- page exceeds record/block limits around very large pages; +- content is not shared with connection; +- block type is unsupported by markdown conversion; +- Notion needs to protect structure such as child page/database content. + +If `truncated` is true, fetch IDs in `unknown_block_ids` by calling the same endpoint on those IDs, or use the block API. A permission-denied unknown block can return `404 object_not_found`. + +File/media URLs in markdown output are signed and expire. Re-fetch for fresh URLs. + +## Update page markdown + +Preferred commands: + +### `update_content` + +Exact search/replace operations. + +```json +{ + "command": { + "type": "update_content", + "content_updates": [ + {"old_str": "Status: draft", "new_str": "Status: final"} + ] + } +} +``` + +Rules: + +- `old_str` matching is exact and case-sensitive. +- Max `content_updates`: 100. +- If `old_str` matches multiple places, request fails unless `replace_all_matches: true` is set deliberately. + +### `replace_content` + +Replace whole page body. + +```json +{ + "command": { + "type": "replace_content", + "new_str": "# New complete body" + } +} +``` + +Deletion guard: + +- Child pages/databases are protected by default. +- Set `allow_deleting_content: true` inside the command when intentionally deleting protected child content. + +Legacy commands still exist but are not preferred: + +- `insert_content` +- `replace_content_range` + +They use selection/range strings and are more fragile; expect deprecation risk. + +Update caveats: + +- Update response returns full page markdown after update. +- Transcript text cannot be updated even if retrieved with `include_transcript=true`. +- Updates cannot target databases or non-page blocks. +- Synced page/unsupported targets can fail validation. + +## Enhanced markdown syntax essentials + +Enhanced markdown extends CommonMark with XML-like tags and attributes. + +Indentation: + +- Use tabs for child nesting. +- Child blocks are one tab deeper. + +Escaping outside code blocks: + +```text +\ * ~ ` $ [ ] < > { } | ^ +``` + +Do not escape inside code blocks. + +Headings: + +```md +# H1 +## H2 +### H3 +#### H4 +``` + +H5/H6 collapse to H4. Toggle headings can use attributes such as `{toggle="true"}`. + +Lists and todos: + +```md +- bullet +1. numbered +- [ ] todo +- [x] done +``` + +Quote with line breaks: + +```md +> first line
second line +``` + +Common block tags: + +```md +
+Toggle title + Nested content +
+ + + Important note + + + + Left + Right + + + + +``` + +Media tags include markdown images and XML-like audio/video/file/pdf tags. Re-fetch file URLs when needed because Notion-hosted file URLs expire. + +Rich text: + +- `**bold**` +- `*italic*` +- `~~strike~~` +- underline via `text` +- inline code +- links +- inline math +- colors via `text` and background colors with `_bg` suffix. + +Mentions: + +- `` +- `` +- `` +- `` +- `` +- `` + +Custom emoji: + +```md +:emoji_name: +``` + +Citations: + +```md +[^https://example.com] +``` + +## When not to use markdown + +Use structured block/data-source APIs when: + +- You need exact block IDs and structural edits. +- You must preserve unsupported blocks. +- You need a precise page-property update. +- You are manipulating files/media objects directly. +- You need to traverse child pages/databases with access checks. diff --git a/skills/productivity/notion/references/official-source-map.md b/skills/productivity/notion/references/official-source-map.md new file mode 100644 index 0000000000000..dd5b4367846dc --- /dev/null +++ b/skills/productivity/notion/references/official-source-map.md @@ -0,0 +1,186 @@ +# Notion API Official Source Map + +Retrieved: 2026-05-18T05:49:54+02:00 + +## Authority order + +1. Public OpenAPI spec: `https://developers.notion.com/openapi.json` + - Retrieved to `/tmp/notion-api-official-md/openapi.json`. + - OpenAPI: 3.1.0. + - Title: Notion API. + - Server: `https://api.notion.com`. + - Auth schemes: `bearerAuth`, `basicAuth`. + - Public documented paths in retrieved spec: 32. +2. Official docs index and markdown corpus: + - `https://developers.notion.com/llms.txt` + - `https://developers.notion.com/llms-full.txt` + - Individual `.md` pages under `developers.notion.com`. +3. Official SDK/repo docs: + - `https://github.com/makenotion/notion-sdk-js` + - `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` + - `https://www.npmjs.com/package/@notionhq/client` +4. Official CLI/MCP/Workers docs and Notion product/developer platform pages. +5. Existing local Hermes Notion skill and plugin usage. +6. Lower-authority hints only: third-party blog posts, examples, random package wrappers. + +## Machine-readable specs + +Public spec: + +```text +https://developers.notion.com/openapi.json +``` + +Paths observed in the 2026-05-18 fetch: + +```text +GET /v1/users/me +GET /v1/users/{user_id} +GET /v1/users +POST /v1/pages +GET /v1/pages/{page_id} +PATCH /v1/pages/{page_id} +POST /v1/pages/{page_id}/move +GET /v1/pages/{page_id}/properties/{property_id} +GET /v1/pages/{page_id}/markdown +PATCH /v1/pages/{page_id}/markdown +GET /v1/blocks/{block_id} +PATCH /v1/blocks/{block_id} +DELETE /v1/blocks/{block_id} +GET /v1/blocks/{block_id}/children +PATCH /v1/blocks/{block_id}/children +GET /v1/data_sources/{data_source_id} +PATCH /v1/data_sources/{data_source_id} +POST /v1/data_sources/{data_source_id}/query +POST /v1/data_sources +GET /v1/data_sources/{data_source_id}/templates +GET /v1/databases/{database_id} +PATCH /v1/databases/{database_id} +POST /v1/databases +POST /v1/search +GET /v1/comments +POST /v1/comments +GET /v1/comments/{comment_id} +PATCH /v1/comments/{comment_id} +DELETE /v1/comments/{comment_id} +GET /v1/file_uploads +POST /v1/file_uploads +POST /v1/file_uploads/{file_upload_id}/send +POST /v1/file_uploads/{file_upload_id}/complete +GET /v1/file_uploads/{file_upload_id} +GET /v1/custom_emojis +GET /v1/views +POST /v1/views +GET /v1/views/{view_id} +PATCH /v1/views/{view_id} +DELETE /v1/views/{view_id} +POST /v1/views/{view_id}/queries +GET /v1/views/{view_id}/queries/{query_id} +DELETE /v1/views/{view_id}/queries/{query_id} +POST /v1/blocks/meeting_notes/query +POST /v1/oauth/token +POST /v1/oauth/revoke +POST /v1/oauth/introspect +``` + +Undocumented spec: + +```text +https://developers.notion.com/openapi-undocumented.json +``` + +Treat this as lower-authority and unstable. It includes agent/teamspace/export/tool endpoints and duplicate/experimental data-source paths. Do not build production integrations against undocumented paths without explicit approval and fresh validation. + +## Official docs corpus fetched + +The local research run saved official markdown pages to: + +```text +/tmp/notion-api-official-md/ +``` + +Important files: + +- `llms.txt` — docs index; includes OpenAPI URLs and Worker/MCP/webhook pages. +- `llms-full.txt` — full official markdown docs corpus. +- `INDEX.tsv` — URL to local file mapping for selected pages. +- `developers-notion-com-reference-versioning-md.md` — version policy. +- `developers-notion-com-reference-changes-by-version-md.md` — breaking versions. +- `developers-notion-com-guides-get-started-upgrade-guide-2026-03-11-md.md` — latest version migration. +- `developers-notion-com-guides-get-started-upgrade-guide-2025-09-03-md.md` — data-source migration. +- `developers-notion-com-reference-request-limits-md.md` — rate and payload limits. +- `developers-notion-com-reference-status-codes-md.md` — status/error codes. +- `developers-notion-com-reference-webhooks-md.md` and `developers-notion-com-reference-webhooks-events-delivery-md.md` — webhooks. +- `raw-githubusercontent-com-makenotion-notion-sdk-js-main-readme-md.md` — official JS SDK README. + +## Current version facts + +Latest REST API version in the fetched docs: `2026-03-11`. + +Breaking versions listed by Notion docs: + +- `2026-03-11` +- `2025-09-03` +- `2022-06-28` +- `2022-02-22` +- `2021-08-16` +- `2021-05-13` + +Version policy: + +- `Notion-Version` header is mandatory. +- URL path `/v1` is not date-versioned and Notion says it does not intend to change those URLs. +- Version bumps cover backwards-incompatible changes. +- Additive changes can happen without a new API version. +- Cursor format is explicitly opaque and may change without a new version. + +## Known conflicts and stale doc spots + +- Some 2025 FAQ/data-source pages still say view management is unsupported; March 2026 changelog and current views docs define `/v1/views`. Treat views guide/changelog/OpenAPI as newer. +- One 2025 upgrade guide snippet labels data-source query as `PATCH`; OpenAPI/reference use `POST /v1/data_sources/{data_source_id}/query`. +- File upload expiry wording conflicts: guide mentions `archived`, object enum says `expired`. Treat `expired` as API status. +- Multipart upload count conflict: create schema allows `number_of_parts` up to 10,000; send schema lists `part_number` 1-1,000. Test before relying on >1,000 parts. +- MCP supported-tools page and changelog disagree about `notion-get-user` and meeting-notes query tools. Monitor before hardcoding MCP tool inventory. +- Worker docs were listed in `llms.txt`, but the focused local fetch did not include detailed Worker docs. Treat Worker SDK details as beta/active and re-fetch before production work. + +## Existing Stockitup evidence + +Read-only scout found no actual Stockitup Notion integration under `/home/snaz/stockitup`: + +- no `api.notion.com` +- no `developers.notion.com` +- no `@notionhq` +- no `NOTION_API_KEY` / `NOTION_API_TOKEN` +- no Notion-specific integration directory or code path + +Only product/documentation mentions were found, such as generic Notion references in Stockitup web/blog material. If Stockitup later needs a Notion integration, choose the owner path deliberately instead of assuming one exists. + +## Local Hermes state found during learning + +Before this refresh: + +- `productivity/notion` existed but was disabled in `~/.hermes/config.yaml`. +- Bare `skill_view("notion")` collided with `creative/popular-web-designs/templates/notion.md`, because legacy flat-skill lookup counted support-file templates as skill candidates. +- The old Notion skill used `2025-09-03`, malformed curl Authorization snippets, dotenv-incompatible `export` guidance, and an unlinked `block-types.md` reference. + +This run patched the loader collision class and refreshed the Notion skill/support files. + +## Monitor candidates + +Good low-noise public monitor targets: + +- `https://developers.notion.com/openapi.json` +- `https://developers.notion.com/llms.txt` +- `https://developers.notion.com/reference/changes-by-version.md` +- `https://developers.notion.com/reference/versioning.md` +- `https://developers.notion.com/page/changelog.md` +- `https://developers.notion.com/reference/request-limits.md` +- `https://developers.notion.com/reference/webhooks.md` +- `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` + +Monitor reasons: + +- API versions and breaking changes are active. +- SDK default version lags latest REST API version. +- Webhook/MCP/Workers surfaces show catalog drift. +- Rate limits explicitly may change by plan or future policy. diff --git a/skills/productivity/notion/references/openapi-generated-inventory-2026-05-18.md b/skills/productivity/notion/references/openapi-generated-inventory-2026-05-18.md new file mode 100644 index 0000000000000..1f97d65ade8c0 --- /dev/null +++ b/skills/productivity/notion/references/openapi-generated-inventory-2026-05-18.md @@ -0,0 +1,198 @@ +# Generated Notion OpenAPI Inventory — 2026-05-18 + +This file is a generated static inventory from the official public Notion OpenAPI spec. It complements the prose references; do not hand-edit endpoint counts without regenerating from the spec. + +## Retrieval receipt + +- Retrieved: `2026-05-18T04:46:46Z` +- URL: `https://developers.notion.com/openapi.json` +- SHA-256: `c781691e6316b679648c83ff8f18a9dd70943fa24dbe35be69d19ff1cb274174` +- Bytes: `786086` +- OpenAPI version: `3.1.0` +- API title: `Notion API` +- API info version: `1.0.0` +- Servers: `https://api.notion.com` +- Paths: `32` +- Operations: `47` +- Component schemas: `505` +- Notion-Version enum in spec: `2026-03-11` + +Regenerate/check with `skills/productivity/notion/scripts/notion_api_surface_snapshot.py`. + +## Operation inventory + +### Blocks + +- `DELETE /v1/blocks/{block_id}` — operationId `delete-a-block`; auth: Bearer; Delete a block +- `GET /v1/blocks/{block_id}` — operationId `retrieve-a-block`; auth: Bearer; Retrieve a block +- `PATCH /v1/blocks/{block_id}` — operationId `update-a-block`; body: application/json; auth: Bearer; Update a block +- `GET /v1/blocks/{block_id}/children` — operationId `get-block-children`; pagination/cursor surface; auth: Bearer; Retrieve block children +- `PATCH /v1/blocks/{block_id}/children` — operationId `patch-block-children`; body: application/json; auth: Bearer; Append block children + +### Comments + +- `GET /v1/comments` — operationId `list-comments`; pagination/cursor surface; auth: Bearer; List comments +- `POST /v1/comments` — operationId `create-a-comment`; body: application/json; auth: Bearer; Create a comment +- `DELETE /v1/comments/{comment_id}` — operationId `delete-a-comment`; auth: Bearer; Delete a comment +- `GET /v1/comments/{comment_id}` — operationId `retrieve-comment`; auth: Bearer; Retrieve a comment +- `PATCH /v1/comments/{comment_id}` — operationId `update-a-comment`; body: application/json; auth: Bearer; Update a comment + +### Custom emojis + +- `GET /v1/custom_emojis` — operationId `list-custom-emojis`; pagination/cursor surface; auth: Bearer; List custom emojis + +### Data sources + +- `POST /v1/data_sources` — operationId `create-a-database`; body: application/json; auth: Bearer; Create a data source +- `GET /v1/data_sources/{data_source_id}` — operationId `retrieve-a-data-source`; auth: Bearer; Retrieve a data source +- `PATCH /v1/data_sources/{data_source_id}` — operationId `update-a-data-source`; body: application/json; auth: Bearer; Update a data source +- `POST /v1/data_sources/{data_source_id}/query` — operationId `post-database-query`; pagination/cursor surface; body: application/json; auth: Bearer; Query a data source +- `GET /v1/data_sources/{data_source_id}/templates` — operationId `list-data-source-templates`; pagination/cursor surface; auth: Bearer; List templates in a data source + +### Databases + +- `POST /v1/databases` — operationId `create-database`; body: application/json; auth: Bearer; Create a database +- `GET /v1/databases/{database_id}` — operationId `retrieve-database`; auth: Bearer; Retrieve a database +- `PATCH /v1/databases/{database_id}` — operationId `update-database`; body: application/json; auth: Bearer; Update a database + +### File uploads + +- `GET /v1/file_uploads` — operationId `list-file-uploads`; pagination/cursor surface; auth: Bearer; List file uploads +- `POST /v1/file_uploads` — operationId `create-file`; body: application/json; auth: Bearer; Create a file upload +- `GET /v1/file_uploads/{file_upload_id}` — operationId `retrieve-file-upload`; auth: Bearer; Retrieve a file upload +- `POST /v1/file_uploads/{file_upload_id}/complete` — operationId `complete-file-upload`; auth: Bearer; Complete a multi-part file upload +- `POST /v1/file_uploads/{file_upload_id}/send` — operationId `upload-file`; body: multipart/form-data; auth: Bearer; Upload a file + +### Meeting notes + +- `POST /v1/blocks/meeting_notes/query` — operationId `query-meeting-notes`; body: application/json; auth: Bearer; Query meeting notes + +### OAuth + +- `POST /v1/oauth/introspect` — operationId `introspect-token`; body: application/json; auth: Basic/client credentials; Introspect a token +- `POST /v1/oauth/revoke` — operationId `revoke-token`; body: application/json; auth: Basic/client credentials; Revoke a token +- `POST /v1/oauth/token` — operationId `create-a-token`; body: application/json; auth: Basic/client credentials; Exchange an authorization code for an access and refresh token + +### Pages + +- `POST /v1/pages` — operationId `post-page`; body: application/json; auth: Bearer; Create a page +- `GET /v1/pages/{page_id}` — operationId `retrieve-a-page`; auth: Bearer; Retrieve a page +- `PATCH /v1/pages/{page_id}` — operationId `patch-page`; body: application/json; auth: Bearer; Update page +- `GET /v1/pages/{page_id}/markdown` — operationId `retrieve-page-markdown`; auth: Bearer; Retrieve a page as markdown +- `PATCH /v1/pages/{page_id}/markdown` — operationId `update-page-markdown`; body: application/json; auth: Bearer; Update a page's content as markdown +- `POST /v1/pages/{page_id}/move` — operationId `move-page`; body: application/json; auth: Bearer; Move a page +- `GET /v1/pages/{page_id}/properties/{property_id}` — operationId `retrieve-a-page-property`; pagination/cursor surface; auth: Bearer; Retrieve a page property item + +### Search + +- `POST /v1/search` — operationId `post-search`; pagination/cursor surface; body: application/json; auth: Bearer; Search by title + +### Users + +- `GET /v1/users` — operationId `get-users`; pagination/cursor surface; auth: Bearer; List all users +- `GET /v1/users/me` — operationId `get-self`; auth: Bearer; Retrieve your token's bot user +- `GET /v1/users/{user_id}` — operationId `get-user`; auth: Bearer; Retrieve a user + +### Views + +- `GET /v1/views` — operationId `list-views`; pagination/cursor surface; auth: Bearer; List views +- `POST /v1/views` — operationId `create-view`; body: application/json; auth: Bearer; Create a view +- `DELETE /v1/views/{view_id}` — operationId `delete-view`; auth: Bearer; Delete a view +- `GET /v1/views/{view_id}` — operationId `retrieve-a-view`; auth: Bearer; Retrieve a view +- `PATCH /v1/views/{view_id}` — operationId `update-a-view`; body: application/json; auth: Bearer; Update a view +- `POST /v1/views/{view_id}/queries` — operationId `create-view-query`; body: application/json; auth: Bearer; Create a view query +- `DELETE /v1/views/{view_id}/queries/{query_id}` — operationId `delete-view-query`; auth: Bearer; Delete a view query +- `GET /v1/views/{view_id}/queries/{query_id}` — operationId `get-view-query-results`; pagination/cursor surface; auth: Bearer; Get view query results + +## First-class operation sets to remember + +### Views + +- `GET /v1/views` — `list-views` +- `POST /v1/views` — `create-view` +- `DELETE /v1/views/{view_id}` — `delete-view` +- `GET /v1/views/{view_id}` — `retrieve-a-view` +- `PATCH /v1/views/{view_id}` — `update-a-view` +- `POST /v1/views/{view_id}/queries` — `create-view-query` +- `DELETE /v1/views/{view_id}/queries/{query_id}` — `delete-view-query` +- `GET /v1/views/{view_id}/queries/{query_id}` — `get-view-query-results` + +### Markdown pages + +- `GET /v1/pages/{page_id}/markdown` — `retrieve-page-markdown` +- `PATCH /v1/pages/{page_id}/markdown` — `update-page-markdown` + +### File uploads + +- `GET /v1/file_uploads` — `list-file-uploads` +- `POST /v1/file_uploads` — `create-file` +- `GET /v1/file_uploads/{file_upload_id}` — `retrieve-file-upload` +- `POST /v1/file_uploads/{file_upload_id}/complete` — `complete-file-upload` +- `POST /v1/file_uploads/{file_upload_id}/send` — `upload-file` + +### OAuth token maintenance + +- `POST /v1/oauth/introspect` — `introspect-token` +- `POST /v1/oauth/revoke` — `revoke-token` +- `POST /v1/oauth/token` — `create-a-token` + +## Webhook event keys in OpenAPI + +- `commentCreated` — `POST` `webhook-comment-created`; Comment created +- `commentDeleted` — `POST` `webhook-comment-deleted`; Comment deleted +- `commentUpdated` — `POST` `webhook-comment-updated`; Comment updated +- `dataSourceContentUpdated` — `POST` `webhook-data-source-content-updated`; Data source content updated +- `dataSourceCreated` — `POST` `webhook-data-source-created`; Data source created +- `dataSourceDeleted` — `POST` `webhook-data-source-deleted`; Data source deleted +- `dataSourceMoved` — `POST` `webhook-data-source-moved`; Data source moved +- `dataSourceSchemaUpdated` — `POST` `webhook-data-source-schema-updated`; Data source schema updated +- `dataSourceUndeleted` — `POST` `webhook-data-source-undeleted`; Data source undeleted +- `databaseContentUpdated` — `POST` `webhook-database-content-updated`; Database content updated +- `databaseCreated` — `POST` `webhook-database-created`; Database created +- `databaseDeleted` — `POST` `webhook-database-deleted`; Database deleted +- `databaseMoved` — `POST` `webhook-database-moved`; Database moved +- `databaseSchemaUpdated` — `POST` `webhook-database-schema-updated`; Database schema updated +- `databaseUndeleted` — `POST` `webhook-database-undeleted`; Database undeleted +- `fileUploadCompleted` — `POST` `webhook-file-upload-completed`; File upload completed +- `fileUploadCreated` — `POST` `webhook-file-upload-created`; File upload created +- `fileUploadExpired` — `POST` `webhook-file-upload-expired`; File upload expired +- `fileUploadUploadFailed` — `POST` `webhook-file-upload-upload-failed`; File upload failed +- `pageContentUpdated` — `POST` `webhook-page-content-updated`; Page content updated +- `pageCreated` — `POST` `webhook-page-created`; Page created +- `pageDeleted` — `POST` `webhook-page-deleted`; Page deleted +- `pageLocked` — `POST` `webhook-page-locked`; Page locked +- `pageMoved` — `POST` `webhook-page-moved`; Page moved +- `pagePropertiesUpdated` — `POST` `webhook-page-properties-updated`; Page properties updated +- `pageTranscriptionBlockTranscriptDeleted` — `POST` `webhook-page-transcription-block-transcript-deleted`; Page transcript deleted +- `pageUndeleted` — `POST` `webhook-page-undeleted`; Page undeleted +- `pageUnlocked` — `POST` `webhook-page-unlocked`; Page unlocked +- `viewCreated` — `POST` `webhook-view-created`; View created +- `viewDeleted` — `POST` `webhook-view-deleted`; View deleted +- `viewUpdated` — `POST` `webhook-view-updated`; View updated + +## Schema enum excerpts worth handling non-exhaustively + +- `baseWebhookPayload.properties.api_version`: `2022-06-28`, `2025-09-03`, `2026-03-11` +- `dataSourceViewObjectResponse.properties.type`: `table`, `board`, `list`, `calendar`, `timeline`, `gallery`, `form`, `chart`, `map`, `dashboard` +- `fileUploadObjectResponse.properties.status`: `pending`, `uploaded`, `expired`, `failed` +- `partialDataSourceViewObjectResponse.properties.type`: `table`, `board`, `list`, `calendar`, `timeline`, `gallery`, `form`, `chart`, `map`, `dashboard` +- `verificationPropertyResponse.properties.state`: `verified`, `expired` +- `verificationPropertyStatusFilter.properties.status`: `verified`, `expired`, `none` +- `viewTypeRequest`: `table`, `board`, `list`, `calendar`, `timeline`, `gallery`, `form`, `chart`, `map`, `dashboard` +- `webhookDatabaseEventEntity.properties.type`: `block`, `database`, `data_source` +- `webhookExternalBlock.properties.type`: `page`, `database`, `block` +- `webhookParentBlock.properties.type`: `space`, `block`, `page`, `database`, `team`, `agent` +- `webhookUpdatedBlock.properties.type`: `page`, `database`, `block` + +## Generated drift flags + +Stale operation IDs detected in official OpenAPI: + +- `POST /v1/data_sources` has operationId `create-a-database`; prefer method+path/canonical docs in generated clients. +- `POST /v1/data_sources/{data_source_id}/query` has operationId `post-database-query`; prefer method+path/canonical docs in generated clients. + +Other standing drift flags: + +- Treat additive fields/types as expected; Notion versioning allows response additions without version bumps. +- Use `code`, not human `message`, for error handling. +- Do not flatten deep `oneOf`/`anyOf`/`allOf` unions into closed enums; keep discriminator + unknown fallback. diff --git a/skills/productivity/notion/references/setup-auth-and-cli.md b/skills/productivity/notion/references/setup-auth-and-cli.md new file mode 100644 index 0000000000000..76cf934a38e82 --- /dev/null +++ b/skills/productivity/notion/references/setup-auth-and-cli.md @@ -0,0 +1,212 @@ +# Notion Setup, Auth, CLI, and SDK + +Sources: + +- `https://developers.notion.com/reference/authentication.md` +- `https://developers.notion.com/guides/get-started/authorization.md` +- `https://developers.notion.com/guides/get-started/internal-connections.md` +- `https://developers.notion.com/guides/get-started/public-connections.md` +- `https://developers.notion.com/guides/get-started/personal-access-tokens.md` +- `https://developers.notion.com/guides/get-started/handling-api-keys.md` +- `https://developers.notion.com/cli/get-started/*.md` +- `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` + +## Token variables + +Hermes environment: + +```dotenv +NOTION_API_KEY=ntn_xxx_or_secret_xxx +``` + +`ntn` environment: + +```dotenv +NOTION_API_TOKEN=ntn_xxx_or_secret_xxx +``` + +If both are needed, duplicate the literal token value. Do not use shell expansion in `~/.hermes/.env`. + +Avoid shell-style `export` lines or variable expansion in `.env`. Hermes' dotenv loader reads literal `KEY=value` lines and does not evaluate shell syntax. + +## REST headers + +Every REST request needs: + +```text +Authorization: Bearer +Notion-Version: 2026-03-11 +``` + +JSON body requests also need: + +```text +Content-Type: application/json +``` + +## Internal connections + +Use for one workspace, server-side/bot automation, and controlled scripts. + +Facts: + +- Created by a Workspace Owner. +- Acts as its own bot identity. +- Has no content access by default. +- Grant access in Developer Portal or Notion UI by adding the connection to a page/database. +- Parent access grants child access. +- Access persists even if the user who added the connection leaves. +- Workspace Owners can see internal connections. + +Failure patterns: + +- `404 object_not_found`: page/data source not shared with the connection. +- `403 restricted_resource`: missing capability or permission. + +## Public OAuth connections + +Use for multi-user products or third-party apps. + +Flow: + +1. Redirect user to Notion authorization URL. +2. Include `client_id`, `redirect_uri`, `response_type=code`, `owner=user`, optional `state`. +3. User selects pages/databases they can fully access. +4. Notion redirects back with temporary `code`. +5. Exchange code at `POST https://api.notion.com/v1/oauth/token` with HTTP Basic auth using `CLIENT_ID:CLIENT_SECRET`. +6. Store `access_token`, `refresh_token`, `bot_id`, workspace metadata, and owner data. + +Use `state` for CSRF protection and app-state restoration. + +Redirect URI rule from docs: + +- Required in token exchange if supplied in authorization URL or if multiple redirect URIs are configured. +- Not allowed in token exchange if exactly one redirect URI is configured and the auth URL did not include it. + +Refresh behavior: + +- Refreshing returns a new access token and a new refresh token. +- Store both returned tokens atomically; a half-written refresh result can strand the authorization record. +- No `expires_in` or token expiry timestamp is documented in the focused official token response; use introspection/refresh/error handling rather than invented expiry math. +- Treat refresh token families as single-owner secrets; do not share across tools without a cutover plan. + +## Personal access tokens + +Use for trusted personal scripts, CLI workflows, Workers, and development. + +Facts: + +- Belongs to one user in one workspace. +- Acts as the user who created it. +- Uses the creator's page/workspace permissions. +- Expires one year after creation. +- Guests/restricted members cannot create PATs or log into `ntn`. +- Admins can view/revoke PATs but cannot reveal another member's secret. +- PATs cannot list all workspace users; use `/v1/users/me` or retrieve the token creator instead. +- Admin policy changes or creator access loss can invalidate API access even if the literal token string still exists. + +Use public OAuth rather than PATs for products used by multiple users. + +## Capabilities + +Main capability families: + +- Content: read, insert, update. +- Comments: read, insert. +- Users: none, without email, with email. + +Capability + access are both required. Sharing a page is not enough if the token lacks the endpoint capability. + +## `ntn` CLI + +Install: + +```bash +curl -fsSL https://ntn.dev | bash +# or +npm install --global ntn +``` + +Requirements from docs: + +- macOS/Linux, x64/arm64. +- npm install requires Node 22+ and npm 10+. +- Windows native support was listed as coming soon; use curl or WSL2. + +Auth modes: + +```bash +ntn login +``` + +Interactive browser login stores workspace-scoped tokens in the OS keychain. Headless login prints a URL, verification code, and `ntn login poll` command. + +For unattended/CI/PAT use: + +```bash +NOTION_API_TOKEN=ntn_xxx ntn api v1/users/me +``` + +Environment variables documented by `ntn`: + +- `NOTION_API_TOKEN` — takes precedence over keychain auth. +- `NOTION_WORKSPACE_ID` — target a specific workspace for one command. +- `NOTION_KEYRING=0` — opt out of OS keychain for `ntn login`; stores plain JSON `auth.json`. +- `NOTION_HOME` — config directory override. +- `NOTION_ENV` — environment selection. + +API requests: + +```bash +ntn api v1/users/me +ntn api v1/data_sources/${DATA_SOURCE_ID}/query page_size:=50 +ntn api v1/pages/${PAGE_ID}/markdown +ntn api v1/pages/${PAGE_ID}/markdown -X PATCH command[type]=replace_content command[new_str]="# New body" +``` + +Inspection: + +```bash +ntn api ls +ntn api v1/pages --spec -X POST +ntn api v1/pages --docs -X POST +ntn --verbose api v1/users/me +``` + +Do not use `--unsafe-verbose` with real tokens unless you intentionally want secrets in logs. + +## Official JS/TS SDK + +Install: + +```bash +npm install @notionhq/client +``` + +Initialize with explicit API version for new code: + +```javascript +const { Client } = require("@notionhq/client"); + +const notion = new Client({ + auth: process.env.NOTION_API_KEY, + notionVersion: "2026-03-11", +}); +``` + +README facts from 2026-05-18 fetch: + +- Runtime: Node >=18; optional TypeScript >=5.9. +- SDK v5+ minimum recommended Notion API version: `2025-09-03`. +- Current SDK supports `2025-09-03` and `2026-03-11`. +- Default SDK version is `2025-09-03`; pass `notionVersion: "2026-03-11"` to opt into latest. +- Retries: 429 for all methods; 500/503 for idempotent GET/DELETE. Defaults to 2 retries with exponential backoff/jitter and respects `Retry-After`. +- Useful helpers: `iteratePaginatedAPI`, `collectPaginatedAPI`, `isFullPage`, `isFullBlock`, `isFullDataSource`, `isNotionClientError`, `APIErrorCode`. + +## Credential safety + +- Treat all Notion tokens like passwords. +- Do not commit or paste tokens into chat, skills, docs, logs, screenshots, or webhook payload examples. +- Use separate tokens per environment/script. +- Rotate/revoke on compromise. +- Store webhook verification tokens like secrets. diff --git a/skills/productivity/notion/references/webhooks-mcp-workers-sdk.md b/skills/productivity/notion/references/webhooks-mcp-workers-sdk.md new file mode 100644 index 0000000000000..ee704ee11f5c1 --- /dev/null +++ b/skills/productivity/notion/references/webhooks-mcp-workers-sdk.md @@ -0,0 +1,272 @@ +# Notion Webhooks, MCP, Workers, CLI Automation, and SDK + +Sources: + +- `https://developers.notion.com/reference/webhooks.md` +- `https://developers.notion.com/reference/webhooks-events-delivery.md` +- `https://developers.notion.com/cli/reference/commands.md` +- `https://developers.notion.com/cli/get-started/*.md` +- `https://developers.notion.com/guides/mcp/*.md` +- `https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md` +- `https://www.notion.com/product/dev` + +## Webhooks + +Purpose: receive near-real-time signals from Notion instead of polling. + +Setup: + +1. In the Notion Developer Portal, open connection settings. +2. Webhooks tab → Create subscription. +3. Enter a public HTTPS endpoint; localhost is not reachable. +4. Choose event types. +5. Notion sends verification POST with `verification_token`. +6. Paste token into the Webhooks UI to activate. + +Changing target URL after verification requires deleting/recreating the subscription. Event type selection can be changed. + +### Signature validation + +Each event includes: + +```text +X-Notion-Signature: sha256= +``` + +Compute HMAC-SHA256 over the exact raw request body using the subscription `verification_token`, then compare timing-safely. + +Validation is optional in docs for no-code platforms but should be required for production custom receivers. + +### Event envelope + +Common fields: + +- `id` +- `timestamp` +- `workspace_id` +- `subscription_id` +- `integration_id` +- `type` +- `authors` +- `accessible_by` for public connections +- `attempt_number` +- `entity` +- `data` + +Events are signals. Fetch latest state by REST API; delivered payload may not be the final state by the time you process it. + +### Event families + +Page events: + +- `page.content_updated` +- `page.created` +- `page.deleted` +- `page.moved` +- `page.properties_updated` +- `page.undeleted` +- `page.locked` +- `page.unlocked` + +Database events: + +- `database.content_updated` +- `database.created` +- `database.deleted` +- `database.moved` +- `database.schema_updated` +- `database.undeleted` + +Data-source events, new after `2025-09-03`: + +- `data_source.content_updated` +- `data_source.created` +- `data_source.deleted` +- `data_source.moved` +- `data_source.schema_updated` +- `data_source.undeleted` + +Comment events: + +- `comment.created` +- `comment.deleted` +- `comment.updated` + +Additional event families exposed by the official OpenAPI/event reference surface: + +- File uploads: `file_upload.created`, `file_upload.completed`, `file_upload.expired`, `file_upload.upload_failed`. +- Views: `view.created`, `view.updated`, `view.deleted`. +- Transcript deletion: `page.transcription_block.transcript_deleted` can still appear in event naming even though REST block type is `meeting_notes` in `2026-03-11`. + +Use the generated OpenAPI inventory for the current event-key list before hardcoding allowlists. + +Delivery facts: + +- Target delivery is within 5 minutes; most events within 1 minute. +- Ordering is not guaranteed; use timestamps and fetch current state. +- High-frequency events are aggregated by entity and delayed briefly. +- Notion retries failed delivery up to 8 times with exponential backoff; final retry is about 24 hours after initial event. + +### Webhook API-version cliffs + +Connection webhook subscriptions carry an API version selected in the Developer Portal, separately from REST request `Notion-Version` headers. + +- Upgrade handlers deliberately. `2025-09-03` introduces data-source event/entity shapes and database/data-source parent changes. Source: `https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03.md`. +- `2026-03-11` is available for webhooks and database-automation webhooks, but the docs say its webhook payloads are identical to `2025-09-03`. Source: `https://developers.notion.com/guides/get-started/upgrade-guide-2026-03-11.md`. +- REST `archived` → `in_trash` does **not** apply to webhook payload fields; keep webhook payload parsers compatible with documented webhook shapes. + +Monitor gaps: + +- `llms.txt` and changelog list file-upload, view, and page-transcript webhook references not fully reflected in the focused delivery table. +- Envelope prose listed entity types narrower than examples. Re-check webhook docs before hardcoding event schema enums. + +## MCP + +Hosted endpoints: + +```text +https://mcp.notion.com/mcp +https://mcp.notion.com/sse # legacy +``` + +For stdio-only clients, docs show `mcp-remote` bridge: + +```bash +npx -y mcp-remote https://mcp.notion.com/mcp +``` + +Auth model: + +- OAuth-user based. +- Does not support bearer-token auth in hosted MCP. +- Access equals authorizing user's access. +- Good for interactive AI clients; not ideal for headless automation. + +Security: + +- Use official domains only. +- Use trusted MCP clients. +- Watch for prompt injection because MCP tools can read/write workspace data. +- Enable human confirmation in external workflows. + +Limitations: + +- Hosted Notion MCP does not currently support file uploads; use File Upload API. +- Search has stricter MCP rate limit than general API: 30/minute in focused docs. + +Supported tool families in 2026 docs: + +- Search/fetch: `notion-search`, `notion-fetch`. +- Pages/content: create/update/move/duplicate pages. +- Data model: create database, update data source, create/update views. +- Query: query data sources or database views depending on plan/features. +- Comments: create/get comments. +- Workspace/users: teams/teamspaces/users/self. + +MCP catalog drift exists: changelog and current supported tools page disagree on some user/meeting-notes tools. Re-check current docs before writing client allowlists. + +## Workers / Developer Platform + +Official docs list Worker pages, and `ntn` exposes Worker commands. Product page describes Workers as hosted TypeScript programs that can provide: + +- syncs: scheduled external API → Notion data source updates; +- tools: callable actions inside Notion/custom agents; +- webhooks: incoming external HTTP events that can update Notion or trigger workflows. + +CLI command families: + +```text +ntn workers new +ntn workers deploy +ntn workers list / ls +ntn workers get +ntn workers create +ntn workers delete / rm +ntn workers exec +ntn workers capabilities list +ntn workers tui / ui +ntn workers sync status/trigger/pause/resume/state +ntn workers env set/list/unset/pull/push +ntn workers oauth start/token/show-redirect-url +ntn workers runs list/logs +ntn workers webhooks list +``` + +Treat Workers and Agent SDK surfaces as beta/alpha unless fresh docs confirm stability. Re-fetch official Worker docs before implementing production Workers. + +## CLI automation surfaces + +`ntn api`: + +- Adds Authorization and Notion-Version headers automatically. +- Uses keychain auth or `NOTION_API_TOKEN`. +- No body means GET; inline body/stdin means POST unless `-X` overrides. +- Inline values use path syntax; `:=` supplies JSON-typed values. +- Query params use `name==value`. +- Header overrides use `Header:Value`. +- Use `--notion-version 2026-03-11` or `NOTION_API_VERSION=2026-03-11` to force version. + +`ntn datasources`: + +- `ntn datasources query ` shortcut. +- `ntn datasources resolve ` to find data source IDs. + +`ntn pages`: + +- markdown get/create/update/trash helpers. + +`ntn files`: + +- create/get/list File Upload helpers. + +Diagnostics: + +- `ntn doctor` +- `ntn update` +- `ntn --verbose api ...` redacts Authorization; `--unsafe-verbose` does not. + +## JS/TS SDK + +Package: + +```text +@notionhq/client +``` + +Key methods and helpers from README/changelog: + +- `notion.users.list()` +- `notion.users.retrieve()` +- `notion.users.me()` / self equivalent in SDK docs +- `notion.pages.create()`, retrieve/update, markdown retrieve/update +- `notion.blocks.children.list()` and append/update/delete block methods +- `notion.dataSources.query()` +- `notion.dataSources.retrieve/update/create` +- `notion.views.*` and `notion.views.queries.*` +- `notion.comments.create/update/delete` +- `notion.customEmojis.list()` +- `notion.blocks.meetingNotes.query()` +- `notion.request({ path, method, body })` for custom endpoints +- `iteratePaginatedAPI()` and `collectPaginatedAPI()` + +Versioning: + +- SDK v5+ supports API versions `2025-09-03` and `2026-03-11`. +- Default SDK version was `2025-09-03` in the 2026-05-18 README fetch. +- Pass `notionVersion: "2026-03-11"` explicitly for new work. + +Retries: + +- Defaults to up to 2 retries. +- Retries 429 for all HTTP methods. +- Retries 500/503 for idempotent GET/DELETE. +- Respects Retry-After. +- Caller still owns create dedupe/idempotency. + +## Monitor candidates + +- Webhook event catalog and per-event pages. +- Hosted MCP supported tool list and rate limits. +- Workers docs and package APIs. +- SDK default API version and supported version matrix. +- Developer Platform alpha/beta package names and pricing/gating. diff --git a/skills/productivity/notion/scripts/notion_api_surface_snapshot.py b/skills/productivity/notion/scripts/notion_api_surface_snapshot.py new file mode 100644 index 0000000000000..58c7534a177f0 --- /dev/null +++ b/skills/productivity/notion/scripts/notion_api_surface_snapshot.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Snapshot official Notion API/developer surfaces for drift monitoring. + +No credentials are used. The script fetches public docs/spec/package metadata, +prints JSON by default, and can compare against a prior JSON snapshot. + +Examples: + python skills/productivity/notion/scripts/notion_api_surface_snapshot.py + python skills/productivity/notion/scripts/notion_api_surface_snapshot.py --baseline /tmp/notion-snapshot.json --markdown +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +from datetime import datetime, timezone +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +USER_AGENT = "Hermes-Notion-API-Surface-Snapshot/1.0 (+https://hermes-agent.nousresearch.com)" +URLS = { + "openapi": "https://developers.notion.com/openapi.json", + "llms": "https://developers.notion.com/llms.txt", + "changelog": "https://developers.notion.com/page/changelog.md", + "changes_by_version": "https://developers.notion.com/reference/changes-by-version.md", + "sdk_readme": "https://raw.githubusercontent.com/makenotion/notion-sdk-js/main/README.md", + "mcp_manifest": "https://www.notion.com/.well-known/mcp.json", + "mcp_oauth_metadata": "https://mcp.notion.com/.well-known/oauth-authorization-server", +} +NPM_PACKAGES = [ + "@notionhq/client", + "ntn", + "@notionhq/workers", + "@notionhq/notion-mcp-server", +] + + +def fetch(url: str) -> tuple[bytes, dict[str, str]]: + req = Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json,text/plain,text/markdown,*/*"}) + with urlopen(req, timeout=45) as response: + data = response.read() + headers = {k.lower(): v for k, v in response.headers.items()} + return data, headers + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def summarize_openapi(data: bytes) -> dict: + spec = json.loads(data.decode("utf-8")) + paths = spec.get("paths", {}) + operations = [] + stale_operation_ids = [] + paginated = [] + for path, path_item in sorted(paths.items()): + for method, op in sorted(path_item.items()): + if method.lower() not in {"get", "post", "patch", "delete", "put"} or not isinstance(op, dict): + continue + params = path_item.get("parameters", []) + op.get("parameters", []) + param_names = [p.get("name") for p in params if isinstance(p, dict)] + opid = op.get("operationId", "") + item = {"method": method.upper(), "path": path, "operationId": opid, "tags": op.get("tags", [])} + operations.append(item) + if "data_sources" in path and "database" in opid.lower(): + stale_operation_ids.append(item) + body = json.dumps(op.get("requestBody", {}), sort_keys=True) + if any(name in {"start_cursor", "page_size"} for name in param_names) or "start_cursor" in body or "page_size" in body: + paginated.append(item) + webhook_events = sorted((spec.get("webhooks") or {}).keys()) + notion_version_parameter = spec.get("components", {}).get("parameters", {}).get("notionVersion", {}) + version_enums = notion_version_parameter.get("schema", {}).get("enum", []) + for path_item in paths.values(): + for p in path_item.get("parameters", []) if isinstance(path_item, dict) else []: + if p.get("name") == "Notion-Version": + version_enums = p.get("schema", {}).get("enum", version_enums) + return { + "openapi": spec.get("openapi"), + "info": spec.get("info", {}), + "servers": spec.get("servers", []), + "path_count": len(paths), + "operation_count": len(operations), + "schema_count": len(spec.get("components", {}).get("schemas", {})), + "notion_version_enum": version_enums, + "operations": operations, + "stale_operation_ids": stale_operation_ids, + "paginated_operations": paginated, + "webhook_events": webhook_events, + } + + +def snapshot() -> dict: + out = {"retrieved_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "sources": {}, "npm": {}} + for name, url in URLS.items(): + try: + data, headers = fetch(url) + source = { + "url": url, + "sha256": sha256(data), + "bytes": len(data), + "etag": headers.get("etag"), + "last_modified": headers.get("last-modified"), + "content_type": headers.get("content-type"), + } + if name == "openapi": + source["summary"] = summarize_openapi(data) + out["sources"][name] = source + except (HTTPError, URLError, TimeoutError, json.JSONDecodeError) as exc: + out["sources"][name] = {"url": url, "error": repr(exc)} + for package in NPM_PACKAGES: + url = "https://registry.npmjs.org/" + package.replace("/", "%2f") + try: + data, headers = fetch(url) + meta = json.loads(data.decode("utf-8")) + latest = meta.get("dist-tags", {}).get("latest") + latest_meta = meta.get("versions", {}).get(latest, {}) if latest else {} + out["npm"][package] = { + "url": url, + "latest": latest, + "dist_tags": meta.get("dist-tags", {}), + "engines": latest_meta.get("engines"), + "repository": latest_meta.get("repository"), + "sha256": sha256(data), + "bytes": len(data), + } + except (HTTPError, URLError, TimeoutError, json.JSONDecodeError) as exc: + out["npm"][package] = {"url": url, "error": repr(exc)} + return out + + +def compare(before: dict, after: dict) -> list[str]: + alerts: list[str] = [] + for name, src in after.get("sources", {}).items(): + old = before.get("sources", {}).get(name, {}) + if src.get("sha256") and old.get("sha256") and src.get("sha256") != old.get("sha256"): + alerts.append(f"source {name} hash changed: {old.get('sha256')} -> {src.get('sha256')}") + old_api = before.get("sources", {}).get("openapi", {}).get("summary", {}) + new_api = after.get("sources", {}).get("openapi", {}).get("summary", {}) + for field in ["path_count", "operation_count", "schema_count", "notion_version_enum", "webhook_events"]: + if old_api.get(field) != new_api.get(field): + alerts.append(f"openapi {field} changed: {old_api.get(field)} -> {new_api.get(field)}") + old_ops = {(o.get("method"), o.get("path"), o.get("operationId")) for o in old_api.get("operations", [])} + new_ops = {(o.get("method"), o.get("path"), o.get("operationId")) for o in new_api.get("operations", [])} + for item in sorted(new_ops - old_ops): + alerts.append(f"openapi operation added: {item[0]} {item[1]} ({item[2]})") + for item in sorted(old_ops - new_ops): + alerts.append(f"openapi operation removed/renamed: {item[0]} {item[1]} ({item[2]})") + for package, meta in after.get("npm", {}).items(): + old = before.get("npm", {}).get(package, {}) + if old.get("latest") and meta.get("latest") and old.get("latest") != meta.get("latest"): + alerts.append(f"npm {package} latest changed: {old.get('latest')} -> {meta.get('latest')}") + return alerts + + +def markdown_report(snap: dict, alerts: list[str] | None = None) -> str: + lines = ["# Notion API Surface Snapshot", "", f"Retrieved: `{snap['retrieved_at']}`", ""] + if alerts is not None: + lines += ["## Alerts", ""] + lines += [f"- {alert}" for alert in alerts] or ["- No monitored high-level changes."] + lines.append("") + api = snap.get("sources", {}).get("openapi", {}).get("summary", {}) + if api: + lines += ["## OpenAPI", ""] + for field in ["openapi", "path_count", "operation_count", "schema_count", "notion_version_enum"]: + lines.append(f"- {field}: `{api.get(field)}`") + lines.append("") + if api.get("stale_operation_ids"): + lines += ["Stale data-source operation IDs:", ""] + for op in api["stale_operation_ids"]: + lines.append(f"- `{op['method']} {op['path']}` — `{op['operationId']}`") + lines.append("") + lines += ["## Package versions", ""] + for package, meta in sorted(snap.get("npm", {}).items()): + lines.append(f"- `{package}`: `{meta.get('latest', 'unknown')}`") + return "\n".join(lines).rstrip() + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--baseline", help="prior JSON snapshot to compare against") + parser.add_argument("--markdown", action="store_true", help="print concise markdown instead of JSON") + args = parser.parse_args() + current = snapshot() + alerts = None + if args.baseline: + with open(args.baseline, "r", encoding="utf-8") as fh: + baseline = json.load(fh) + alerts = compare(baseline, current) + if args.markdown: + sys.stdout.write(markdown_report(current, alerts)) + else: + if alerts is not None: + current["alerts"] = alerts + json.dump(current, sys.stdout, indent=2, sort_keys=True) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/tools/test_skills_tool.py b/tests/tools/test_skills_tool.py index 9502467546e14..cb5779a05e2f8 100644 --- a/tests/tools/test_skills_tool.py +++ b/tests/tools/test_skills_tool.py @@ -347,6 +347,21 @@ def test_view_existing_skill(self, tmp_path): assert result["name"] == "my-skill" assert "Step 1" in result["content"] + def test_legacy_flat_lookup_ignores_support_files(self, tmp_path): + """A template/reference named .md must not collide with a real skill.""" + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill(tmp_path, "notion", category="productivity") + template_dir = tmp_path / "creative" / "popular-web-designs" / "templates" + template_dir.mkdir(parents=True) + (template_dir / "notion.md").write_text("# Not a skill\n") + + raw = skill_view("notion") + + result = json.loads(raw) + assert result["success"] is True + assert result["name"] == "notion" + assert result["path"] == "productivity/notion/SKILL.md" + def test_skill_view_applies_template_vars(self, tmp_path): with ( patch("tools.skills_tool.SKILLS_DIR", tmp_path), diff --git a/tools/skills_tool.py b/tools/skills_tool.py index df6361ba59a17..dc2e6ca62a266 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -1004,9 +1004,21 @@ def _record(sd: Optional[Path], smd: Path) -> None: _record(found_skill_md.parent, found_skill_md) # Strategy 3: legacy flat .md files anywhere under the dir. + # Do not treat skill support files as top-level skills. References, + # templates, scripts, and assets commonly contain files named after + # the thing they document (for example templates/notion.md), and + # counting those as legacy skills creates false collisions with real + # directory skills. + support_dirs = {"references", "templates", "scripts", "assets"} for found_md in search_dir.rglob(f"{name}.md"): - if found_md.name != "SKILL.md": - _record(None, found_md) + if found_md.name == "SKILL.md": + continue + try: + if any(part in support_dirs for part in found_md.relative_to(search_dir).parts[:-1]): + continue + except ValueError: + pass + _record(None, found_md) if len(candidates) > 1: paths = [str(smd) for _, smd in candidates] diff --git a/website/docs/reference/skills-catalog.md b/website/docs/reference/skills-catalog.md index c5b205f521d6a..f83e424a73450 100644 --- a/website/docs/reference/skills-catalog.md +++ b/website/docs/reference/skills-catalog.md @@ -144,7 +144,7 @@ If a skill is missing from this list but present in the repo, the catalog is reg | [`linear`](/docs/user-guide/skills/bundled/productivity/productivity-linear) | Linear: manage issues, projects, teams via GraphQL + curl. | `productivity/linear` | | [`maps`](/docs/user-guide/skills/bundled/productivity/productivity-maps) | Geocode, POIs, routes, timezones via OpenStreetMap/OSRM. | `productivity/maps` | | [`nano-pdf`](/docs/user-guide/skills/bundled/productivity/productivity-nano-pdf) | Edit PDF text/typos/titles via nano-pdf CLI (NL prompts). | `productivity/nano-pdf` | -| [`notion`](/docs/user-guide/skills/bundled/productivity/productivity-notion) | Notion API + ntn CLI: pages, databases, markdown, Workers. | `productivity/notion` | +| [`notion`](/docs/user-guide/skills/bundled/productivity/productivity-notion) | Use when reading, writing, integrating, or troubleshooting Notion through the REST API, ntn CLI, MCP, webhooks, pages, data sources, markdown, blocks, comments, files, or official JS SDK. | `productivity/notion` | | [`ocr-and-documents`](/docs/user-guide/skills/bundled/productivity/productivity-ocr-and-documents) | Extract text from PDFs/scans (pymupdf, marker-pdf). | `productivity/ocr-and-documents` | | [`powerpoint`](/docs/user-guide/skills/bundled/productivity/productivity-powerpoint) | Create, read, edit .pptx decks, slides, notes, templates. | `productivity/powerpoint` | | [`teams-meeting-pipeline`](/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline) | Operate the Teams meeting summary pipeline via Hermes CLI — summarize meetings, inspect pipeline status, replay jobs, manage Microsoft Graph subscriptions. | `productivity/teams-meeting-pipeline` | diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md index 80487d6b88fae..f81be682bb03a 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md @@ -1,14 +1,14 @@ --- -title: "Notion — Notion API + ntn CLI: pages, databases, markdown, Workers" +title: "Notion" sidebar_label: "Notion" -description: "Notion API + ntn CLI: pages, databases, markdown, Workers" +description: "Use when reading, writing, integrating, or troubleshooting Notion through the REST API, ntn CLI, MCP, webhooks, pages, data sources, markdown, blocks, commen..." --- {/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} # Notion -Notion API + ntn CLI: pages, databases, markdown, Workers. +Use when reading, writing, integrating, or troubleshooting Notion through the REST API, ntn CLI, MCP, webhooks, pages, data sources, markdown, blocks, comments, files, or official JS SDK. ## Skill metadata @@ -16,11 +16,12 @@ Notion API + ntn CLI: pages, databases, markdown, Workers. |---|---| | Source | Bundled (installed by default) | | Path | `skills/productivity/notion` | -| Version | `2.0.0` | -| Author | community | +| Version | `2.1.0` | +| Author | community + Hermes Agent | | License | MIT | | Platforms | linux, macos, windows | -| Tags | `Notion`, `Productivity`, `Notes`, `Database`, `API`, `CLI`, `Workers` | +| Tags | `Notion`, `Productivity`, `Notes`, `Data Sources`, `API`, `CLI`, `Markdown`, `Files`, `Webhooks`, `MCP` | +| Related skills | `web-apis`, `oauth-sota`, [`webhook-subscriptions`](/docs/user-guide/skills/bundled/devops/devops-webhook-subscriptions) | ## Reference: full SKILL.md @@ -30,434 +31,359 @@ The following is the complete skill definition that Hermes loads when this skill # Notion -Talk to Notion two ways. Same integration token works for both — pick by what's available. +## Overview -◆ **`ntn` CLI** — Notion's official CLI. Shorter syntax, one-line file uploads, required for Workers. macOS + Linux only as of May 2026 (Windows support "coming soon"). **Default when installed.** -◆ **HTTP + curl** — works everywhere including Windows. **Default fallback** when `ntn` isn't installed. +Use this skill for Notion's developer surfaces: the Notion REST API, `ntn` CLI, official JS/TS SDK, webhooks, MCP, page markdown, blocks, data sources, comments, views, and file uploads. -## Setup +This skill was refreshed from official Notion docs on 2026-05-18. Highest-authority sources are: -### 1. Get an integration token (required for both paths) +1. `https://developers.notion.com/openapi.json` — public OpenAPI 3.1 spec for the documented REST API. +2. `https://developers.notion.com/llms.txt` and official `.md` pages under `developers.notion.com`. +3. `https://github.com/makenotion/notion-sdk-js` / `@notionhq/client` README. +4. `ntn` CLI docs and Notion product/dev pages for Workers, MCP, and alpha/beta surfaces. -1. Create an integration at https://notion.so/my-integrations -2. Copy the API key (starts with `ntn_` or `secret_`) -3. Store in `~/.hermes/.env`: - ``` - NOTION_API_KEY=ntn_your_key_here - ``` -4. **Share target pages/databases with the integration** in Notion: page menu `...` → `Connect to` → your integration name. Without this, the API returns 404 for that page even though it exists. +Default REST API version for new code: **`2026-03-11`**. -### 2. Install `ntn` (preferred path on macOS / Linux) +Important version cliffs: -```bash -# Recommended -curl -fsSL https://ntn.dev | bash +- `2025-09-03`: Notion databases became containers; rows/schema live in **data sources**. +- `2026-03-11`: use `position` instead of legacy `after` for block insertion; use `in_trash` instead of `archived`; use `meeting_notes` instead of `transcription`. -# Or via npm (needs Node 22+, npm 10+) -npm install --global ntn +## When to Use -ntn --version # verify -``` +Use this skill when the task mentions: -**Skip `ntn login` — use the integration token instead.** This works headlessly, no browser needed: -```bash -export NOTION_API_TOKEN=$NOTION_API_KEY # ntn reads NOTION_API_TOKEN -export NOTION_KEYRING=0 # don't try to use the OS keychain -``` +- Notion pages, databases, data sources, views, blocks, comments, users, search, files, markdown, webhooks, MCP, Workers, or `ntn`. +- `api.notion.com`, `Notion-Version`, `@notionhq/client`, `@notionhq/workers`, `mcp.notion.com`, or `NOTION_API_KEY` / `NOTION_API_TOKEN`. +- Building a Notion integration, syncing Notion data, exporting/importing content, receiving Notion events, or troubleshooting Notion permissions. -Add those exports to your shell profile (or to `~/.hermes/.env`) so every session inherits them. +Do not use this skill for generic note-taking advice unrelated to the API. -### 3. Choose path at runtime +## Credentials and Setup -```bash -if command -v ntn >/dev/null 2>&1; then - # use ntn -else - # fall back to curl -fi -``` +### Hermes env variable -Windows users: skip step 2 entirely until native `ntn` ships — Path B works fine. If you want CLI ergonomics now, install `ntn` inside WSL2. +Hermes currently treats `NOTION_API_KEY` as the configured secret name. Keep that as the primary Hermes prerequisite. -## API Basics +`~/.hermes/.env` uses dotenv-style literal `KEY=value` lines. Do **not** put `export ...` lines in it, and do **not** rely on `$NOTION_API_KEY` expansion inside the file. -`Notion-Version: 2025-09-03` is required on all HTTP requests. `ntn` handles this for you. In this version, what users call "databases" are called **data sources** in the API. +```dotenv +NOTION_API_KEY=ntn_xxx_or_secret_xxx +``` -## Path A — `ntn` CLI (preferred, macOS / Linux) +For `ntn`, Notion's own CLI reads `NOTION_API_TOKEN`. If you need both Hermes and `ntn`, duplicate the literal token: -### Raw API calls (shorthand for curl) -```bash -ntn api v1/users # GET -ntn api v1/pages parent[page_id]=abc123 \ # POST with inline body - properties[title][0][text][content]="Notes" -ntn api v1/pages/abc123 -X PATCH archived:=true # PATCH; := is non-string (bool/num/null) +```dotenv +NOTION_API_KEY=ntn_xxx_or_secret_xxx +NOTION_API_TOKEN=ntn_xxx_or_secret_xxx ``` -Syntax notes: -- `key=value` — string fields -- `key[nested]=value` — nested object fields -- `key:=value` — typed assignment (booleans, numbers, null, arrays) +Tokens are opaque strings. Newer public API tokens use the `ntn_` prefix; old `secret_` tokens can still work. Do not regex-validate token formats. -### Search -```bash -ntn api v1/search query="page title" -``` +### Choose a client path -### Read page metadata -```bash -ntn api v1/pages/{page_id} -``` +Prefer by availability: -### Read page as Markdown (agent-friendly) -```bash -ntn api v1/pages/{page_id}/markdown -``` +1. **`ntn` CLI** on macOS/Linux for one-shot terminal work, file uploads, endpoint inspection, and Workers commands. +2. **HTTP + curl** everywhere, including Windows and minimal environments. +3. **Official JS/TS SDK** for TypeScript/Node integrations. +4. **Hosted Notion MCP** for interactive AI-user access, not headless bearer-token automation. -### Read page content as blocks -```bash -ntn api v1/blocks/{page_id}/children -``` +`ntn` install: -### Create page from Markdown ```bash -ntn api v1/pages \ - parent[page_id]=xxx \ - properties[title][0][text][content]="Notes from meeting" \ - markdown="# Agenda - -- Q3 roadmap -- Hiring" +curl -fsSL https://ntn.dev | bash +# or, with Node 22+ and npm 10+ +npm install --global ntn +ntn --version ``` -### Patch a page with Markdown -```bash -ntn api v1/pages/{page_id}/markdown -X PATCH \ - markdown="## Update +`ntn` is documented for macOS/Linux x64/arm64; Windows support was still listed as “coming soon” in the 2026-05-18 docs. Use curl on Windows, or use `ntn` in WSL2. -Shipped the prototype." -``` +### Minimal HTTP smoke test -### Query a database (data source) -```bash -ntn api v1/data_sources/{data_source_id}/query -X POST \ - filter[property]=Status filter[select][equals]=Active -``` +Only run this if a real token is intentionally configured: -For complex queries with `sorts`, multiple filter clauses, or compound logic, pipe JSON in: ```bash -echo '{"filter": {"property": "Status", "select": {"equals": "Active"}}, "sorts": [{"property": "Date", "direction": "descending"}]}' | \ - ntn api v1/data_sources/{data_source_id}/query -X POST --json - +curl -sS "https://api.notion.com/v1/users/me" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" ``` -### File uploads (one-liner — biggest CLI win) +For JSON bodies add: + ```bash -ntn files create < photo.png -ntn files create --external-url https://example.com/photo.png -ntn files list + -H "Content-Type: application/json" ``` -Compare to the 3-step HTTP flow (create upload → PUT bytes → reference). +## Auth and Access Model -### Useful env vars -| Var | Effect | -|---|---| -| `NOTION_API_TOKEN` | Auth token (overrides keychain) — set this to your integration token | -| `NOTION_KEYRING=0` | File-based creds at `~/.config/notion/auth.json` instead of OS keychain | -| `NOTION_WORKSPACE_ID` | Skip the workspace picker prompt | +REST requests use bearer-token auth plus a mandatory `Notion-Version` header. -## Path B — HTTP + curl (cross-platform, default on Windows) +Token types: -All requests share this pattern: +- **Internal connection token**: static bot token for one workspace. It has no content access by default. Grant access from the Developer Portal Content access tab or in Notion UI by adding the connection to a page/database. Parent access flows to children. +- **Public connection token**: OAuth 2.0 access token per authorizing user/workspace. Use `state` for CSRF/app-state. Store `bot_id` as the primary authorization key. Refreshing returns a new access token and a new refresh token. +- **Personal access token (PAT)**: static user-scoped token. Acts as the user who created it, expires after one year, and follows that user's workspace/page permissions. Good for trusted scripts and CLI; use OAuth for multi-user products. -```bash -curl -s -X GET "https://api.notion.com/v1/..." \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" -``` +Capabilities matter and do not override page/workspace permissions: -On Windows the `curl` shipped with Windows 10+ works as-is. PowerShell users can also use `Invoke-RestMethod`. +- read content: retrieve/query pages, blocks, data sources, markdown. +- insert content: create pages/data sources, append blocks, attach uploaded files. +- update content: update/trash/restore pages/blocks, markdown updates, schema changes where permitted. +- read/insert comments: comment APIs. +- user info capabilities: user list/retrieve and optional email visibility. -### Search -```bash -curl -s -X POST "https://api.notion.com/v1/search" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"query": "page title"}' -``` +Gotchas: -### Read page metadata -```bash -curl -s "https://api.notion.com/v1/pages/{page_id}" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" -``` +- `404 object_not_found` often means “not shared with this token/connection,” not true absence. +- `403 restricted_resource` means missing capability or permission. +- Relation targets and linked data sources usually must also be shared with the connection. +- PATs cannot list all workspace users; retrieve the token's bot/current user instead. -### Read page as Markdown (agent-friendly) +## API Basics -Easier to feed to a model than block JSON. +Base URL: -```bash -curl -s "https://api.notion.com/v1/pages/{page_id}/markdown" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" +```text +https://api.notion.com ``` -### Read page content as blocks (when you need structure) -```bash -curl -s "https://api.notion.com/v1/blocks/{page_id}/children" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" -``` +Public OpenAPI spec: -### Create page from Markdown +```text +https://developers.notion.com/openapi.json +``` -`POST /v1/pages` accepts a `markdown` body param. +Undocumented/lower-authority spec: -```bash -curl -s -X POST "https://api.notion.com/v1/pages" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "parent": {"page_id": "xxx"}, - "properties": {"title": [{"text": {"content": "Notes from meeting"}}]}, - "markdown": "# Agenda\n\n- Q3 roadmap\n- Hiring\n\n## Decisions\n- Ship MVP Friday" - }' +```text +https://developers.notion.com/openapi-undocumented.json ``` -### Patch a page with Markdown -```bash -curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}/markdown" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"markdown": "## Update\n\nShipped the prototype."}' -``` +Rules: -### Create page in a database (typed properties) -```bash -curl -s -X POST "https://api.notion.com/v1/pages" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "parent": {"database_id": "xxx"}, - "properties": { - "Name": {"title": [{"text": {"content": "New Item"}}]}, - "Status": {"select": {"name": "Todo"}} - } - }' -``` +- Use HTTPS and JSON unless the endpoint explicitly says multipart/form-data. +- `Notion-Version: 2026-03-11` for new REST work. +- The URL namespace remains `/v1`; date-versioning is only the header. +- IDs are UUIDs; Notion accepts dashed or undashed forms. +- Empty strings are not supported. Use `null` to unset nullable strings. +- Ignore unknown response fields; additive changes can occur without version bumps. +- Treat cursors as opaque. Pass `next_cursor` back as `start_cursor`; never parse it. +- For full endpoint/current-schema inventory, load `references/openapi-generated-inventory-2026-05-18.md`; for drift/codegen traps, load `references/deep-edge-cases-and-codegen.md`. -### Query a database (data source) -```bash -curl -s -X POST "https://api.notion.com/v1/data_sources/{data_source_id}/query" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{ - "filter": {"property": "Status", "select": {"equals": "Active"}}, - "sorts": [{"property": "Date", "direction": "descending"}] - }' -``` +## Common Task Recipes + +### Search for pages or data sources by title + +Use search as discovery, not authoritative inventory. Search is eventually consistent and not exhaustive. -### Create a database ```bash -curl -s -X POST "https://api.notion.com/v1/data_sources" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ +curl -sS -X POST "https://api.notion.com/v1/search" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ -H "Content-Type: application/json" \ - -d '{ - "parent": {"page_id": "xxx"}, - "title": [{"text": {"content": "My Database"}}], - "properties": { - "Name": {"title": {}}, - "Status": {"select": {"options": [{"name": "Todo"}, {"name": "Done"}]}}, - "Date": {"date": {}} - } - }' + -d '{"query":"roadmap","filter":{"property":"object","value":"page"},"page_size":10}' ``` -### Update page properties +For database-like objects in `2025-09-03+`, search/filter results use `data_source`, not `database`. + +### Read a page for an agent + +Prefer markdown for model-readable page content: + ```bash -curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"properties": {"Status": {"select": {"name": "Done"}}}}' +curl -sS "https://api.notion.com/v1/pages/${PAGE_ID}/markdown" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" ``` -### Append blocks to a page +If `truncated` is true or `unknown_block_ids` are returned, fetch those block/page IDs or fall back to structured block traversal. + +### Query a data source + +Database containers are not row tables anymore. Discover the data source ID first from `GET /v1/databases/{database_id}`, then query: + ```bash -curl -s -X PATCH "https://api.notion.com/v1/blocks/{page_id}/children" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ +curl -sS -X POST "https://api.notion.com/v1/data_sources/${DATA_SOURCE_ID}/query" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ -H "Content-Type: application/json" \ - -d '{ - "children": [ - {"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": "Hello from Hermes!"}}]}} - ] - }' + -d '{"page_size":50}' ``` -### File uploads (3-step flow) -```bash -# 1. Create upload -curl -s -X POST "https://api.notion.com/v1/file_uploads" \ - -H "Authorization: Bearer $NOTION_API_KEY" \ - -H "Notion-Version: 2025-09-03" \ - -H "Content-Type: application/json" \ - -d '{"filename": "photo.png", "content_type": "image/png"}' +For very large data sources, avoid blind full polling. Query pagination can cap around 10,000 results; use filters and webhooks. -# 2. PUT bytes to the upload_url returned above -curl -s -X PUT "{upload_url}" --data-binary @photo.png +### Work with saved views -# 3. Reference {file_upload_id} in a page/block payload -``` +Use views when the user's saved filter/sort/layout is the desired truth. Use data-source query when you need ad-hoc filters/sorts. -## Property Types +- List views with `GET /v1/views?database_id=...` or `GET /v1/views?data_source_id=...`; retrieve full configuration with `GET /v1/views/{view_id}`. +- Create views with `POST /v1/views` using `data_source_id` plus exactly one placement parent: `database_id`, dashboard `view_id`, or `create_database`. +- View queries are cached result sets: create `/queries`, paginate, then delete/free them. They expire after about 15 minutes and cannot accept extra filters/sorts. +- Dashboard view `configuration.rows` is read-only; change dashboard layout by creating/deleting widget views, respecting the documented widget-row limits. -Common property formats for database items: +### Create a page from markdown -- **Title:** `{"title": [{"text": {"content": "..."}}]}` -- **Rich text:** `{"rich_text": [{"text": {"content": "..."}}]}` -- **Select:** `{"select": {"name": "Option"}}` -- **Multi-select:** `{"multi_select": [{"name": "A"}, {"name": "B"}]}` -- **Date:** `{"date": {"start": "2026-01-15", "end": "2026-01-16"}}` -- **Checkbox:** `{"checkbox": true}` -- **Number:** `{"number": 42}` -- **URL:** `{"url": "https://..."}` -- **Email:** `{"email": "user@example.com"}` -- **Relation:** `{"relation": [{"id": "page_id"}]}` +Under a normal page: -## API Version 2025-09-03 — Databases vs Data Sources +```bash +curl -sS -X POST "https://api.notion.com/v1/pages" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"parent":{"type":"page_id","page_id":"PAGE_ID"},"markdown":"# Notes\n\n- Decision: ship"}' +``` -- **Databases became data sources.** Use `/data_sources/` endpoints for queries and retrieval. -- **Two IDs per database:** `database_id` and `data_source_id`. - - `database_id` when creating pages: `parent: {"database_id": "..."}` - - `data_source_id` when querying: `POST /v1/data_sources/{id}/query` -- Search returns databases as `"object": "data_source"` with the `data_source_id` field. +Under a data source, use `data_source_id` as parent and provide properties matching the schema. -## Notion Workers (advanced, requires `ntn`) +### Create or apply page templates -Workers are TypeScript programs Notion hosts for you. One worker can expose any combination of: -- **Syncs** — pull data from external APIs into a Notion database on a schedule (default 30 min). -- **Tools** — appear as callable tools inside Notion's Custom Agents. -- **Webhooks** — receive HTTP events from external services (GitHub, Stripe, etc.) and act in Notion. +- List data-source templates with `GET /v1/data_sources/{data_source_id}/templates`; templates are ordinary Notion pages surfaced for that data source. +- For page create/update, `template` is `{ "type": "default" }` or `{ "type": "template_id", "template_id": "..." }`; include `timezone` when `@now` / `@today` should resolve predictably. +- Template application is asynchronous: read the page after the request before depending on merged content or properties. +- Do not combine `template` with `children`; avoid mixing template with markdown/content modes unless the current schema docs prove that exact combination. +- Updating an existing page with `erase_content: true` is destructive replacement before template application. -**Plan / platform gating:** -- CLI works on all plans. **Deploying Workers requires Business or Enterprise.** -- `ntn` is macOS/Linux only as of May 2026. Windows users need WSL2 or to wait for native support. -- Free through August 11, 2026; metered on Notion credits after. +### Update page markdown -### Minimal Worker +Prefer exact search/replace updates with `update_content`, or whole-page replacement with `replace_content`. Legacy `insert_content` and `replace_content_range` still exist but are not preferred. ```bash -ntn workers new my-worker # scaffold -cd my-worker -# Edit src/index.ts -ntn workers deploy --name my-worker +curl -sS -X PATCH "https://api.notion.com/v1/pages/${PAGE_ID}/markdown" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"command":{"type":"update_content","content_updates":[{"old_str":"Status: draft","new_str":"Status: final"}]}}' ``` -`src/index.ts`: -```typescript -import { Worker } from "@notionhq/workers"; +If the old string matches multiple places, set `replace_all_matches: true` deliberately. If the page changed, exact matching fails instead of silently editing the wrong text. -const worker = new Worker(); -export default worker; +### Append blocks -worker.tool("greet", { - title: "Greet a User", - description: "Returns a friendly greeting", - inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, - execute: async ({ name }) => `Hello, ${name}!`, -}); -``` +Use `position`, not legacy `after`: -### Webhook capability - -```typescript -worker.webhook("onGithubPush", { - title: "GitHub Push Handler", - execute: async (events, { notion }) => { - for (const event of events) { - // event.body, event.rawBody (for signature verification), event.headers - console.log("got delivery", event.deliveryId); - } - }, -}); +```bash +curl -sS -X PATCH "https://api.notion.com/v1/blocks/${BLOCK_ID}/children" \ + -H "Authorization: Bearer ${NOTION_API_KEY}" \ + -H "Notion-Version: 2026-03-11" \ + -H "Content-Type: application/json" \ + -d '{"position":{"type":"end"},"children":[{"object":"block","type":"paragraph","paragraph":{"rich_text":[{"text":{"content":"Hello from Hermes"}}]}}]}' ``` -After deploy: `ntn workers webhooks list` shows the URL Notion generates. Treat that URL as a secret — anyone with it can POST events unless you add signature verification. +Limits: max 100 block children per append request, max two nesting levels in one request. + +### File uploads -### Worker lifecycle commands +Prefer `ntn` when installed: ```bash -ntn workers deploy -ntn workers list -ntn workers exec -d '{"name": "world"}' -ntn workers sync trigger # run a sync now -ntn workers sync pause -ntn workers env set GITHUB_WEBHOOK_SECRET=... -ntn workers runs list # recent invocations -ntn workers runs logs -ntn workers webhooks list +ntn files create < ./photo.png ``` -When asked to build a Worker, scaffold with `ntn workers new`, write the code in `src/index.ts`, set any secrets with `ntn workers env set`, and deploy. Notion's docs at https://developers.notion.com/workers cover the full API surface. +HTTP flow is not a PUT-to-presigned-URL flow. It is: -## Notion-Flavored Markdown (used by `/markdown` endpoints) +1. `POST /v1/file_uploads` to create a File Upload object. +2. `POST /v1/file_uploads/{file_upload_id}/send` with multipart form field `file`. +3. Attach `{ "type": "file_upload", "file_upload": { "id": "..." } }` in a supported page/block/property API. -Standard CommonMark plus XML-like tags for Notion-specific blocks. Use **tabs** for indentation. +Attach uploaded files within one hour. File download URLs expire after one hour; re-fetch the file/page/block object to refresh URLs. -**Blocks beyond CommonMark:** -``` - - Ship the MVP by **Friday**. - +## Webhooks, MCP, Workers, SDK -
-Toggle title - Children indented one tab -
+Webhooks: - - Left side - Right side - +- Created in the connection's Developer Portal Webhooks tab, not by a REST endpoint in the public OpenAPI spec. +- Target must be public HTTPS; localhost is not reachable. +- Verification POST includes `verification_token`; paste it into the portal to activate. +- Validate event bodies with `X-Notion-Signature`: HMAC-SHA256 over the exact raw JSON body using the subscription verification token. +- Events are signals. Fetch latest state by REST API; ordering is not guaranteed. +- Notion retries failed deliveries up to 8 times with exponential backoff, with final retry around 24 hours after the first event. +- Webhook subscriptions have their own Developer Portal API version. Upgrade handlers deliberately: `2025-09-03` changes database/data-source event shapes, while `2026-03-11` webhook payloads are documented as identical to `2025-09-03`; REST `archived` → `in_trash` does not apply to webhook payload fields. - -``` +MCP: -**Inline:** -- Mentions: ``, `Title`, `` -- Underline: `text` -- Color: `text` or block-level `{color="blue"}` on the first line -- Math: inline `$x^2$`, block `$$ ... $$` -- Citations: `[^https://example.com]` +- Hosted endpoints: `https://mcp.notion.com/mcp` and legacy `https://mcp.notion.com/sse`. +- Requires user OAuth; it does not support bearer-token headless auth. +- MCP access equals the authorizing Notion user's access. +- File uploads are not currently supported by hosted Notion MCP; use File Upload API. -**Colors:** `gray brown orange yellow green blue purple pink red`, plus `*_bg` variants for backgrounds. +Workers / Developer Platform: -Headings 5/6 collapse to H4. Multiple `>` lines render as separate quote blocks — use `
` inside a single `>` for multi-line quotes. +- `ntn` has Workers commands for scaffold/deploy/list/exec/sync/env/oauth/runs/webhooks. +- Workers are hosted TypeScript programs with syncs, tools, and incoming webhooks. Treat deeper Worker/Agent SDK docs as active beta/alpha surfaces; re-check official docs before production work. -## Choosing the Right Path +JS/TS SDK: -| Task | mac / Linux | Windows | -|---|---|---| -| Read/write pages, search, query databases | `ntn api ...` | curl | -| Read a page for an agent to summarize | `ntn api v1/pages/{id}/markdown` | curl `/markdown` endpoint | -| Upload a file | `ntn files create < file` | 3-step HTTP flow | -| One-off API exploration | `ntn api ...` | curl | -| Build a sync / webhook / agent tool hosted by Notion | `ntn workers ...` | WSL2 + `ntn workers ...` | +```bash +npm install @notionhq/client +``` + +SDK v5+ supports `2025-09-03` and `2026-03-11`, but defaults to `2025-09-03`. Opt into latest explicitly: -## Notes +```javascript +const { Client } = require("@notionhq/client"); +const notion = new Client({ + auth: process.env.NOTION_API_KEY, + notionVersion: "2026-03-11", +}); +``` -- Page/database IDs are UUIDs (with or without dashes — both accepted). -- Rate limit: ~3 requests/second average. The CLI doesn't bypass this. -- The API cannot set database **view** filters — that's UI-only. -- Use `"is_inline": true` when creating data sources to embed them in a page. -- Always pass `-s` to curl to suppress progress bars (cleaner agent output). -- Pipe JSON through `jq` when reading: `... | jq '.results[0].properties'`. -- Notion also ships an MCP server now (`Notion MCP`, ~91% more token-efficient on DB ops than the previous version) — wire it via Hermes' MCP support if you want streaming Notion access from inside a session, but the paths above are enough for most one-shot tasks. +SDK retries 429 for all methods and 500/503 for idempotent GET/DELETE by default; still design caller-level idempotency for creates. + +## Request Mechanics + +- Rate limit: average 3 requests/second per connection; respect `Retry-After` on 429. +- Payload limit: 500KB overall and 1000 block elements per request. +- Array limits: many block/rich-text arrays cap at 100 elements. +- Rich text content/link URL: 2000 chars; equation: 1000 chars; URL: 2000 chars; email/phone: 200 chars; relation/people: 100 entries. +- `GET` paginated endpoints take query params; `POST` paginated endpoints take JSON body params. +- `page_size` max is generally 100. +- Error response programmatic field is `code`; message text may change without version bump. +- Retry transient 502/503/504 with backoff and jitter. For data-source query 503, reduce `page_size` and narrow filters/sorts. +- Do not blind-retry `POST /v1/pages` or `POST /v1/file_uploads` after ambiguous network failures; no idempotency-key is documented. + +## References + +Load these support files for deeper work: + +- `references/api-knownness-packet.md` — web-apis knownness packet and handoff summary. +- `references/official-source-map.md` — source ranking, OpenAPI/spec receipts, monitor candidates, Stockitup/local state. +- `references/setup-auth-and-cli.md` — tokens, OAuth/PAT/internal connections, `ntn`, curl, SDK setup. +- `references/api-2026-03-11.md` — version cliffs, endpoint map, pagination/errors/limits. +- `references/data-sources-and-pages.md` — data source/database/page/block/property/view model. +- `references/markdown-workflows.md` — page markdown create/read/update and enhanced markdown syntax. +- `references/block-types.md` — block payload examples and traversal/update rules. +- `references/file-uploads.md` — File Upload API and `ntn files` workflows. +- `references/webhooks-mcp-workers-sdk.md` — webhooks, MCP, Workers, JS SDK, monitor gaps. +- `references/openapi-generated-inventory-2026-05-18.md` — generated endpoint/operation/webhook/schema inventory from official OpenAPI. +- `references/deep-edge-cases-and-codegen.md` — second-pass edge cases, docs drift, codegen rules, and monitor inputs. +- `scripts/notion_api_surface_snapshot.py` — no-credential public docs/spec/package snapshot tool for drift monitoring. + +## Common Pitfalls + +1. **Using stale `2025-09-03` examples for new code.** Use `2026-03-11` unless compatibility requires an older version. +2. **Calling databases rows.** Databases are containers; data sources hold schema and rows. +3. **Using `database_id` for new row parents/relations.** Use `data_source_id` after `2025-09-03`. +4. **Using `archived`, `after`, or `transcription` in new REST payloads.** Use `in_trash`, `position`, and `meeting_notes`; treat meeting-notes blocks as read/query-only, not create/update payload targets. +5. **Assuming 404 means absent.** It often means the page/data source is not shared with the token owner/connection. +6. **Forgetting capabilities.** Token page access is not enough if the connection lacks read/insert/update/comment/user capability. +7. **Using search as inventory.** Search is eventually consistent and not exhaustive. Prefer known IDs or data-source queries. +8. **Pasting shell exports into `.env`.** Hermes `.env` wants literal `KEY=value` lines. +9. **Copying malformed docs/examples blindly.** Normalize curl Authorization headers and version headers yourself. +10. **Caching signed file URLs.** They expire; re-fetch objects for fresh URLs. +11. **Treating MCP as headless integration auth.** Hosted MCP is OAuth-user oriented; use REST/SDK/Workers for unattended automation. +12. **Retrying creates without dedupe.** Notion does not document idempotency keys for create APIs. + +## Verification Checklist + +- [ ] Chosen source: OpenAPI/spec, official `.md` docs, SDK README, CLI docs, or explicitly lower-authority product/beta docs. +- [ ] REST calls include `Authorization: Bearer …` and `Notion-Version: 2026-03-11`. +- [ ] Content access was granted/shared to the internal/public connection, or a PAT is intentionally used. +- [ ] Required capabilities match the endpoint. +- [ ] Data-source work uses `data_source_id`; old database endpoints are treated as deprecated/compat paths. +- [ ] Pagination follows `has_more` / `next_cursor`; cursors are opaque. +- [ ] 429 honors `Retry-After`; transient 502/503/504 use backoff; unsafe creates are deduped. +- [ ] File uploads attach within one hour and signed file URLs are not cached long-term. +- [ ] Webhook receiver validates `X-Notion-Signature` before production use. +- [ ] No credentials, webhook tokens, page customer data, or file URLs are pasted into durable public artifacts.