Notion API
Use the Notion API via curl to create, read, update pages, databases (data sources), and blocks. No extra tools needed — just curl and a Notion API key.
Prerequisites
- Create an integration at https://notion.so/my-integrations
- Copy the API key (starts with
ntn_orsecret_) - Store it in
~/.hermes/.env:NOTION_API_KEY=ntn_your_key_here - Important: Share target pages/databases with your integration in Notion (click "..." → "Connect to" → your integration name)
API Basics
All requests use this pattern:
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"
The Notion-Version header is required. This skill uses 2025-09-03 (latest). In this version, databases are called "data sources" in the API.
Common Operations
Search
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"}'
Get Page
curl -s "https://api.notion.com/v1/pages/{page_id}" \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2025-09-03"
Get Page Content (blocks)
curl -s "https://api.notion.com/v1/blocks/{page_id}/children" \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2025-09-03"
Create Page in a Database
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"}}
}
}'
Query a Database
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"}]
}'
Create a Database
curl -s -X POST "https://api.notion.com/v1/data_sources" \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2025-09-03" \
-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": {}}
}
}'
Update Page Properties
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"}}}}'
Add Content to a Page
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" \
-H "Content-Type: application/json" \
-d '{
"children": [
{"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": "Hello from Hermes!"}}]}}
]
}'
Property Types
Common property formats for database items:
- 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"}]}
Key Differences in API Version 2025-09-03
- Databases → Data Sources: Use
/data_sources/endpoints for queries and retrieval - Two IDs: Each database has both a
database_idand adata_source_id- Use
database_idwhen creating pages (parent: {"database_id": "..."}) - Use
data_source_idwhen querying (POST /v1/data_sources/{id}/query)
- Use
- Search results: Databases return as
"object": "data_source"with theirdata_source_id
Notes
- Page/database IDs are UUIDs (with or without dashes) Rate limit: ~3 requests/second average.
- File uploads via API are capped at ~5MB. For larger files, upload to Google Drive and link from the Notion page instead. Update existing link paragraphs; don't try to replace file blocks.
- The API cannot set database view filters — that's UI-only
- Use
is_inline: truewhen creating data sources to embed them in pages - Add
-sflag to curl to suppress progress bars (cleaner output for Hermes) - Pipe output through
jqfor readable JSON:... | jq '.results[0].properties'
Page Visibility & Parent Selection (CRITICAL)
Pages created via the API are NOT automatically shared with workspace members. public_url is null by default. If the user says "can't see it", the parent page you chose is either inaccessible to them or the page wasn't shared.
DB creation pitfall: 404 "Could not find page"
Even when a page appears in search results, the integration may not have write access to it (only read at workspace level). To create a database when no accessible parent page exists:
- Create a temporary placeholder page in any database you CAN write to (test first)
- Create the target database under that page:
parent: {page_id: placeholder_id} - Archive the placeholder afterward
The placeholder page appears in the writable database as a side effect — archive it to clean up.
Token resolution pitfall
When NOTION_TOKEN="$NOTION...Y" — the value is a reference to another env var, not the actual token. Source the env file and resolve $-prefixed values before using.
When creating a page for a user, you MUST use a parent page they can already see. The API cannot share pages — that's UI-only.
- Search first:
POST /v1/searchwith{"filter": {"property": "object", "value": "page"}} - Filter by accessible: Only use pages that HAVE a
urlfield — pages without URLs are database entries the user can't navigate to directly - Pick a known parent: Use a page the user explicitly references (e.g., an existing project page), or one with a recognizable title
- Create under that parent: Use its ID as
parent.page_id
After creating
- Return the workspace URL format (e.g.,
https://weblyfe.notion.site/Page-Title-{id_without_dashes}), NOTapp.notion.com - Verify with
GET /v1/pages/{id}— 200 means the integration can see it - If the user still can't see it, they need to: Notion Settings → Connections → find the integration → ensure workspace access → share parent page via
...→Connect to
API Key Masking in Hermes
Hermes masks API keys (ntn_..., sk-...) in terminal commands and write_file content. This silently corrupts:
- Bash:
Bearer $NOTION_API_KEY→Bearer ***→ 401 Unauthorized - Python f-strings:
f"Bearer ***→ syntax error on write
Workaround: Use Python scripts with string concatenation ("Bearer " + key), read keys from ~/.hermes/.env with os.path.expanduser(), and strip quotes from values. See references/hermes-masking.md.
Scripts
scripts/create_page.py— Create a Notion page from a markdown file, auto-discovering an accessible parent. Usage:python3 scripts/create_page.py "Title" content.md [parent_id]