Install
openclaw skills install notion-api-skillNotion API integration with managed OAuth. Query databases, search pages, and read workspace content. Write operations require explicit user confirmation of the target resource and connection. Use this skill when users want to interact with Notion workspaces, databases, or pages. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).
openclaw skills install notion-api-skillAccess the Notion API with managed OAuth authentication. Query databases, search pages, and read workspace content. All write operations (creating, updating, or deleting pages, blocks, and databases) require explicit user confirmation specifying the target resource and connection before execution.
CLI:
maton notion search 'meeting notes'
maton api '/notion/v1/search'
Python:
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'query': 'meeting notes'}).encode()
req = urllib.request.Request('https://api.maton.ai/notion/v1/search', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
req.add_header('Notion-Version', '2025-09-03')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
https://api.maton.ai/notion/{native-api-path}
Maton proxies requests to api.notion.com and automatically injects your OAuth token. Write operations (POST, PATCH, DELETE) must only be executed after the user confirms the target page/database ID and intended connection.
All Notion API requests require the version header:
Notion-Version: 2025-09-03
NPM:
npm install -g @maton-ai/cli
Homebrew:
brew install maton-ai/cli/maton
CLI:
maton login # Opens browser for API key
maton login --interactive # Skip browser, paste API key directly
maton whoami # Show current auth state
Manual:
MATON_API_KEY:export MATON_API_KEY="YOUR_API_KEY"
Manage your Notion OAuth connections at https://api.maton.ai.
CLI:
maton connection list notion --status ACTIVE
maton api -X GET /connections -f app=notion -f status=ACTIVE
Python:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections?app=notion&status=ACTIVE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
CLI:
maton connection create notion
maton api /connections -f app=notion
Python:
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'app': 'notion'}).encode()
req = urllib.request.Request('https://api.maton.ai/connections', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
CLI:
maton connection view {connection_id}
maton api /connections/{connection_id}
Python:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections/{connection_id}')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Response:
{
"connection": {
"connection_id": "{connection_id}",
"status": "ACTIVE",
"creation_time": "2025-12-08T07:20:53.488460Z",
"last_updated_time": "2026-01-31T20:03:32.593153Z",
"url": "https://connect.maton.ai/?session_token=...",
"app": "notion",
"method": "OAUTH2",
"metadata": {}
}
}
Open the returned url in a browser to complete OAuth authorization.
CLI:
maton connection delete {connection_id}
maton api -X DELETE /connections/{connection_id}
Python:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections/{connection_id}', method='DELETE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If you have multiple Notion connections, specify which one to use:
CLI:
maton notion search 'meeting notes' --connection {connection_id}
maton api /notion/v1/search --connection {connection_id}
Python:
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'query': 'meeting notes'}).encode()
req = urllib.request.Request('https://api.maton.ai/notion/v1/search', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
req.add_header('Notion-Version', '2025-09-03')
req.add_header('Maton-Connection', '{connection_id}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If you have multiple connections, always specify the connection to ensure requests go to the intended account.
In API version 2025-09-03, databases and data sources are separate:
| Concept | Use For |
|---|---|
| Database | Creating databases, getting data source IDs |
| Data Source | Querying, updating schema, updating properties |
Use GET /databases/{id} to get the data_sources array, then use /data_sources/ endpoints:
{
"object": "database",
"id": "abc123",
"data_sources": [
{"id": "def456", "name": "My Database"}
]
}
Search for pages:
POST /notion/v1/search
Content-Type: application/json
Notion-Version: 2025-09-03
{
"query": "meeting notes",
"filter": {"property": "object", "value": "page"}
}
Example:
maton notion search 'meeting notes' --filter page
Search for data sources:
POST /notion/v1/search
Content-Type: application/json
Notion-Version: 2025-09-03
{
"filter": {"property": "object", "value": "data_source"}
}
Example:
maton notion search --filter data_source
GET /notion/v1/data_sources/{dataSourceId}
Notion-Version: 2025-09-03
Example:
maton notion data-source view <dataSourceId>
POST /notion/v1/data_sources/{dataSourceId}/query
Content-Type: application/json
Notion-Version: 2025-09-03
{
"filter": {
"property": "Status",
"select": {"equals": "Active"}
},
"sorts": [
{"property": "Created", "direction": "descending"}
],
"page_size": 100
}
Example:
maton notion data-source query <dataSourceId> \
--filter '{"property":"Status","select":{"equals":"Active"}}' \
--sorts '[{"property":"Created","direction":"descending"}]' \
--page-size 100
PATCH /notion/v1/data_sources/{dataSourceId}
Content-Type: application/json
Notion-Version: 2025-09-03
{
"title": [{"type": "text", "text": {"content": "Updated Title"}}],
"properties": {
"NewColumn": {"rich_text": {}}
}
}
Example:
maton notion data-source update <dataSourceId> \
--body '{"title":[{"type":"text","text":{"content":"Updated Title"}}],"properties":{"NewColumn":{"rich_text":{}}}}'
GET /notion/v1/databases/{databaseId}
Notion-Version: 2025-09-03
Example:
maton notion database view <databaseId>
POST /notion/v1/databases
Content-Type: application/json
Notion-Version: 2025-09-03
{
"parent": {"type": "page_id", "page_id": "PARENT_PAGE_ID"},
"title": [{"type": "text", "text": {"content": "New Database"}}],
"properties": {
"Name": {"title": {}}
}
}
Example:
maton notion database create --parent-page PARENT_PAGE_ID --title 'New Database'
In API version 2025-09-03, POST /databases only accepts the title property — any other entries in properties are silently dropped. To define a schema, follow up with PATCH /data_sources/{dataSourceId} (see Update Data Source) using the data_sources[0].id returned by the create call.
GET /notion/v1/pages/{pageId}
Notion-Version: 2025-09-03
Example:
maton notion page view <pageId>
POST /notion/v1/pages
Content-Type: application/json
Notion-Version: 2025-09-03
{
"parent": {"page_id": "PARENT_PAGE_ID"},
"properties": {
"title": {"title": [{"text": {"content": "New Page"}}]}
}
}
Example:
maton notion page create --parent-page PARENT_PAGE_ID --title 'New Page'
POST /notion/v1/pages
Content-Type: application/json
Notion-Version: 2025-09-03
{
"parent": {"data_source_id": "DATA_SOURCE_ID"},
"properties": {
"Name": {"title": [{"text": {"content": "New Page"}}]},
"Status": {"select": {"name": "Active"}}
}
}
Example:
maton notion page create --data-source DATA_SOURCE_ID --title 'New Page' \
--properties '{"Status":{"select":{"name":"Active"}}}'
PATCH /notion/v1/pages/{pageId}
Content-Type: application/json
Notion-Version: 2025-09-03
{
"properties": {
"Status": {"select": {"name": "Done"}}
}
}
Example:
maton notion page update {pageId} --properties '{"Status":{"select":{"name":"Done"}}}'
PATCH /notion/v1/pages/{pageId}
Content-Type: application/json
Notion-Version: 2025-09-03
{
"icon": {"type": "emoji", "emoji": "🚀"}
}
Example:
maton notion page update {pageId} --icon 🚀
Or with an image URL:
maton notion page update {pageId} --icon https://example.com/icon.png
PATCH /notion/v1/pages/{pageId}
Content-Type: application/json
Notion-Version: 2025-09-03
{
"archived": true
}
Example:
maton notion page archive {pageId}
GET /notion/v1/blocks/{blockId}/children
Notion-Version: 2025-09-03
Example:
maton notion block children <blockId>
PATCH /notion/v1/blocks/{blockId}/children
Content-Type: application/json
Notion-Version: 2025-09-03
{
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{"type": "text", "text": {"content": "New paragraph"}}]
}
}
]
}
Example:
maton notion block append <blockId> \
--children '[{"object":"block","type":"paragraph","paragraph":{"rich_text":[{"type":"text","text":{"content":"New paragraph"}}]}}]'
DELETE /notion/v1/blocks/{blockId}
Notion-Version: 2025-09-03
Example:
maton notion block delete <blockId>
GET /notion/v1/users
Notion-Version: 2025-09-03
Example:
maton notion user list
GET /notion/v1/users/me
Notion-Version: 2025-09-03
Example:
maton notion whoami
equals, does_not_equalcontains, does_not_containstarts_with, ends_withis_empty, is_not_emptygreater_than, less_thanparagraph, heading_1, heading_2, heading_3bulleted_list_item, numbered_list_itemto_do, code, quote, dividerNotion uses cursor-based pagination. The CLI automatically paginates with '--paginate'.
Example:
maton notion data-source query <dataSourceId> --paginate
# Search for pages matching a query
maton notion search 'roadmap'
# View a specific page
maton notion page view 0123456789abcdef0123456789abcdef
# Query a data source with a filter
maton notion data-source query <dataSourceId> --filter '{"property":"Status","select":{"equals":"Active"}}'
# Filter with jq — e.g., only pages (responses are wrapped in {"results": [...]})
# Note: --jq requires --json
maton notion search 'roadmap' --json --jq '.results | map(select(.object == "page"))'
const response = await fetch('https://api.maton.ai/notion/v1/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MATON_API_KEY}`,
'Notion-Version': '2025-09-03'
},
body: JSON.stringify({ query: 'meeting' })
});
import os
import requests
response = requests.post(
'https://api.maton.ai/notion/v1/search',
headers={
'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}',
'Notion-Version': '2025-09-03'
},
json={'query': 'meeting'}
)
GET /databases/{id} to get the data_sources array containing data source IDsPOST /databases endpointarchived: truecurl -g when URLs contain brackets (fields[], sort[], records[]) to disable glob parsingjq or other commands, environment variables like $MATON_API_KEY may not expand correctly in some shell environments. You may get "Invalid API key" errors when piping.| Status | Meaning |
|---|---|
| 400 | Missing Notion connection |
| 401 | Invalid or missing Maton API key |
| 429 | Rate limited (10 req/sec per account) |
| 4xx/5xx | Passthrough error from Notion API |
CLI:
maton whoami
maton connection list
Manual:
MATON_API_KEY environment variable is set:echo $MATON_API_KEY
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
notion. For example:https://api.maton.ai/notion/v1/searchhttps://api.maton.ai/v1/search