notion-api-automation
Notion API Automation
Why this exists
Notion API work is easy to get “mostly right” and still fail in production due to:
- Versioning:
Notion-Versionis required and features differ by version. - Database vs data source split: modern integrations must work with
data_source_idfor “rows”. - Properties vs content: page properties are not the same as page block content.
- Pagination + throttling: most listing endpoints are paginated; rate limits are tight.
- Validation schema: property values must match the data source schema exactly.
This skill provides reliable, repeatable workflows and drop-in request patterns.
Quick start
Required environment variables (defaults are safe)
NOTION_TOKEN(preferred) orNOTION_API_KEY/NOTION_API_TOKENNOTION_VERSION(default:2025-09-03)- Optional:
NOTION_BASE_URL(default:https://api.notion.com)
Smoke test (HTTP)
Use the “bot user” endpoint to verify the token + headers:
curl https://api.notion.com/v1/users/me \
-H "Authorization: Bearer $NOTION_TOKEN" \
-H "Notion-Version: ${NOTION_VERSION:-2025-09-03}"
Smoke test (Python script)
python scripts/notion_request.py GET /v1/users/me
JS/TS (preferred when you’re already in Node)
npm i @notionhq/client
import { Client } from "@notionhq/client"
const notion = new Client({
auth: process.env.NOTION_TOKEN,
notionVersion: process.env.NOTION_VERSION ?? "2025-09-03",
})
// 1) Search for a page/database by title fragment
const search = await notion.search({
query: "Project Alpha",
page_size: 5,
})
// 2) Retrieve a page (properties only)
const page = await notion.pages.retrieve({ page_id: search.results[0].id })
console.log(page.id)
If the SDK lacks a newer endpoint, use raw HTTP (fetch/axios) with the same headers.
Operating rules (do these every time)
-
Always send these headers
Authorization: Bearer <token>Notion-Version: 2025-09-03(or whatever the integration is pinned to)Content-Type: application/jsonfor JSON bodies
-
Respect the “database vs data source” model
- A database is a container; it has one or more data sources.
- Use
data_source_idto query rows and to move pages into a database.
-
Know what you’re reading
GET /v1/pages/{page_id}returns properties, not the page body.- Page body is in blocks:
GET /v1/blocks/{block_id}/children(usepage_idasblock_id).
-
No empty strings
- To clear strings and similar fields, set them to
null, not"".
- To clear strings and similar fields, set them to
-
Pagination is mandatory for “list/query/search”
- Loop on
has_more+next_cursor - Send
start_cursorandpage_size(often 1–100)
- Loop on
-
Throttle + retry
- Handle HTTP
429 rate_limited; honourRetry-After. - Prefer sequential writes unless you have a proper rate limiter.
- Handle HTTP
-
Validate after writing
- Retrieve the updated resource and confirm the expected fields changed.
Core workflows (copy + tick)
A) Identify IDs (page/database/data_source/block)
- If you have a Notion URL: extract the 32-char/UUID id (script:
scripts/notion_extract_id.py) - If you only have a title: use
POST /v1/searchto find candidate pages/databases - If you have a
database_idand need rows: callGET /v1/databases/{database_id}and choose adata_source_id
See: references/versioning-data-sources.md and references/endpoints.md.
B) Read data
- Page properties:
GET /v1/pages/{page_id} - Page content:
GET /v1/blocks/{page_id}/children(+ recurse ifhas_children) - Data source rows:
POST /v1/data_sources/{data_source_id}/query
See: references/blocks.md and references/pagination-rate-limits.md.
C) Write data (properties)
- Create page (row) under a data source:
POST /v1/pageswithparent.type=data_source_id - Update page properties:
PATCH /v1/pages/{page_id} - Archive/restore page: set
archivedorin_trash
See: references/property-values.md.
D) Write data (content / blocks)
- Append children blocks:
PATCH /v1/blocks/{block_id}/children - Insert not-at-end: use
"after": "<block_id>"when appending - Update a block (supported block types only):
PATCH /v1/blocks/{block_id} - Delete/trash a block:
DELETE /v1/blocks/{block_id}(destructive)
See: references/blocks.md.
E) Move pages (re-parent)
- Move under another page:
POST /v1/pages/{page_id}/movewithparent.type=page_id - Move into a database:
POST /v1/pages/{page_id}/movewithparent.type=data_source_id - When moving into a database: use data source id, not database id
See: references/recipes.md.
F) Templates
- List templates:
GET /v1/data_sources/{data_source_id}/templates - Create page with a template (recommended for consistent scaffolding)
- If your integration must act after the template applies: wait/poll or use webhooks
See: references/recipes.md.
Troubleshooting (fast diagnosis)
Use the response "code" + HTTP status:
400 missing_version→ you forgotNotion-Version401 unauthorized→ token invalid / wrong env var403 restricted_resource→ integration lacks access or capabilities for that endpoint404 object_not_found→ wrong ID or page/database not shared with the integration429 rate_limited→ throttle; retry afterRetry-After400 validation_error→ request body schema mismatch (often property values)409 conflict_error→ retry; re-fetch latest ids/objects if necessary
For a full list, see references/endpoints.md and the official status codes docs.
Safety tiers for write operations
| Tier | Auto-OK? | Examples |
|---|---|---|
| SAFE | Yes | Search, retrieve page, query data source |
| CAUTION | Yes (validate afterwards) | Update properties, append blocks |
| DANGEROUS | Only if explicitly requested | erase_content, delete blocks, bulk-archive/trash, bulk-move |
If a user asks for a bulk destructive operation but hasn’t specified scope (which pages? which data source?), do the read-only discovery steps first and present a plan + exact targets.
THE EXACT PROMPT — Notion API execution plan
You are operating on a Notion workspace via the public Notion API.
Goal:
- [state the user goal precisely]
Constraints:
- Use Notion-Version 2025-09-03 unless the user specifies otherwise.
- Respect pagination and the 3 rps average rate limit; handle 429 with Retry-After.
- Treat “database” vs “data source” correctly (query rows via data_sources).
- Avoid destructive actions unless the user explicitly asks; if destructive, enumerate targets first.
- Validate by retrieving the changed objects after writes.
Plan:
1) Identify required IDs (page_id, database_id, data_source_id, block_id).
2) Verify access (retrieve or search). If 404, explain how to share the resource with the integration.
3) Choose endpoints + request bodies.
4) Implement using @notionhq/client if available; otherwise raw HTTP with correct headers.
5) Run a validation loop: retrieve → compare expected fields → report results.
References (load as-needed)
references/endpoints.md— endpoint map + “what to call when”references/versioning-data-sources.md— versioning + database/data_source modelreferences/property-values.md— property JSON patterns for create/updatereferences/blocks.md— block JSON patterns for contentreferences/pagination-rate-limits.md— pagination + retry patternsreferences/recipes.md— multi-step workflows (move pages, bulk ops, templates)
Quick grep:
grep -RIn "data_source_id" references/
grep -RIn "pages/{page_id}/move" references/
grep -RIn "Retry-After" references/
Scripts
python scripts/notion_request.py METHOD PATH [--json JSON] [--json-file FILE] [--paginate]python scripts/notion_extract_id.py "<notion url or id>"