---
name: notion
description: "Notion API via curl: pages, databases, blocks, search."
version: 1.0.0
author: community
license: MIT
metadata:
  hermes:
    tags: [Notion, Productivity, Notes, Database, API]
    homepage: https://developers.notion.com
prerequisites:
  env_vars: [NOTION_API_KEY]
---

# 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

1. Create an integration at https://notion.so/my-integrations
2. Copy the API key (starts with `ntn_` or `secret_`)
3. Store it in `~/.hermes/.env`:
   ```
   NOTION_API_KEY=ntn_your_key_here
   ```
4. **Important:** Share target pages/databases with your integration in Notion (click "..." → "Connect to" → your integration name)

## Secure Credential Handling

When the user provides an API key during a session:

1. **Accept it immediately** if explicitly offered. Do not ask "are you sure?" — the user has decided.
2. **Store it in `~/.hermes/.env`** (or the profile `.env`) with `chmod 600`, not in chat-visible memory.
3. **Never echo the key back** in the response. Confirm with asterisks: `NOTION_API_KEY=***`.
4. **Do not store the raw key in agent memory.** Only store the fact that it exists and its location.
5. **If the skill's `setup_needed` flag still triggers**, ignore it — the token is in `.env` and will be loaded by the next session. The skill checker only validates env presence at skill load time, not runtime injection.

### Practical `.env` pattern used in session 2026-05-07

```python
from hermes_tools import write_file
import os

env_path = "/DATA/AppData/hermes/.env"  # or ~/.hermes/.env
with open(env_path, "a") as f:
    f.write("NOTION_API_KEY=ntn_...\n")
os.chmod(env_path, 0o600)
```

Then verify with an immediate API call before confirming success to the user.

---

## API Version Pitfall

Notion API versions are **not fully compatible**. If you get `invalid_request` or `validation_error` when creating blocks, the API version likely mismatched.

- **Version `2022-06-28`** — Older, stable. Works reliably for block creation (`PATCH /blocks/{id}/children`). Use this if you encounter block-level errors.
- **Version `2025-09-03`** — Latest. Uses `data_sources` instead of `databases`. Good for queries, but may have stricter block validation.

**Rule of thumb:** Use `2025-09-03` for database queries and `2022-06-28` for block writing if you hit issues.

## Python urllib Pattern (No External Libs)

When curl is cumbersome and you want pure Python:

```python
import json, urllib.request, os

NOTION_API_KEY = os.popen("grep NOTION_API_KEY /DATA/AppData/hermes/.env | cut -d'=' -f2").read().strip()
PAGE_ID = "your-page-id"

def notion_api(method, endpoint, payload=None, version="2022-06-28"):
    url = f"https://api.notion.com/v1{endpoint}"
    headers = {
        "Authorization": f"Bearer {NOTION_API_KEY}",
        "Notion-Version": version,
        "Content-Type": "application/json"
    }
    data = json.dumps(payload).encode() if payload else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode())
```

## Bulk Block Insertion (Chunking)

The Notion API rejects requests with more than **100 blocks** in the `children` array. Always chunk:

```python
blocks = [...]  # your list of block dicts
for i in range(0, len(blocks), 100):
    chunk = blocks[i:i+100]
    notion_api("PATCH", f"/blocks/{PAGE_ID}/children", {"children": chunk})
```

## Replacing Page Content (Delete → Rebuild)

To fully replace a page's contents (e.g., user says "alles was da ist kannst du löschen"):

1. **Read existing blocks**: `GET /v1/blocks/{page_id}/children`
2. **Delete each block**: `DELETE /v1/blocks/{block_id}` (Note: `child_page` and `child_database` blocks cannot be deleted via API — skip them and warn the user.)
3. **Insert new blocks**: `PATCH /v1/blocks/{page_id}/children` with chunked arrays.

```python
# Step 1: Read
existing = notion_api("GET", f"/blocks/{PAGE_ID}/children")
# Step 2: Delete (skip undeletable types)
for block in existing.get("results", []):
    if block["type"] not in ("child_page", "child_database"):
        notion_api("DELETE", f"/blocks/{block['id']}")
# Step 3: Insert new (chunked)
```

**Pitfall:** `child_page` and `child_database` blocks throw `validation_error` on DELETE. Skip them silently or notify the user.

---

All requests use this pattern:

```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"
```

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

```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"}'
```

### Get Page

```bash
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)

```bash
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

```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"}}
    }
  }'
```

### Query a Database

```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"}]
  }'
```

### 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" \
  -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

```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"}}}}'
```

### Add Content to a Page

```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" \
  -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_id` and a `data_source_id`
  - Use `database_id` when creating pages (`parent: {"database_id": "..."}`)
  - Use `data_source_id` when querying (`POST /v1/data_sources/{id}/query`)
- **Search results:** Databases return as `"object": "data_source"` with their `data_source_id`

## Notes

- Page/database IDs are UUIDs (with or without dashes)
- Rate limit: ~3 requests/second average
- The API cannot set database view filters — that's UI-only
- Use `is_inline: true` when creating data sources to embed them in pages
- Add `-s` flag to curl to suppress progress bars (cleaner output for Hermes)
- Pipe output through `jq` for readable JSON: `... | jq '.results[0].properties'`
