The real problem
Why this is harder than it looks
Most teams start with Notion's internal integration — a static token, one workspace, done in twenty minutes. The problems start when you try to make it work for real users across their own workspaces.
For a multi-user product you need a public integration, which means registering a Notion OAuth app with a company website, privacy policy, and terms of service, then waiting for Notion's review. Until it clears, your integration only works for workspaces you've manually added as test users. Then there's the capabilities system: unlike most OAuth APIs where scopes are declared at authorization time, Notion's read, write, insert, and comment capabilities are configured on the integration itself. Call an endpoint without the right capability toggled on and you get a 403 — not a scope error. If you add a capability after users have already authorized, existing tokens won't automatically reflect it.
Even after a user completes OAuth, your integration only has access to the pages and databases they explicitly selected in Notion's consent picker. API calls to anything outside that selection return a "page not found" error, not a permissions error. In a multi-tenant deployment, distinguishing "this page doesn't exist" from "this user didn't share it" requires extra logic you have to write and maintain.
None of this is impossible. But it's weeks of work before your agent does anything useful. And then you do it again for the next connector. Scalekit handles the OAuth flow, token lifecycle, and revocation detection — your agent names a tool and passes parameters, nothing else.
Capabilities
What your agent can do with Notion
Once connected, your agent has 10 pre-built tools covering pages, databases, and comments:
- Search the entire workspace: full-text search across all pages and databases the user has shared with the integration
- Create and read pages: create pages inside databases or as children of existing pages; fetch page content and metadata by ID
- Query and manage databases: list rows with filters and sorting, insert new rows, create databases with custom schemas
- Introspect database schemas at runtime: fetch full property definitions so your agent adapts to each user's actual structure without hardcoding field names
- Read and write comments: create top-level comments on pages or replies within discussion threads; fetch comment threads by block ID
Setup context
What we're building
This guide connects a knowledge assistant agent to Notion — helping team members search docs, log updates, and manage structured project data without leaving your product.
🤖
Example agent
Knowledge assistant reading and writing Notion pages and databases on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own workspace. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Notion secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="notion",
identifier="user_notion_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct capabilities, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="notion",
identifier="user_notion_456"
)
# Redirect user → Notion consent screen (page picker)
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves and selects their pages, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If a user revokes access from Notion's Settings → Connections, the account moves to REVOKED
— no silent failures. Check account.status before critical operations.
Bring Your Own Credentials — required for production
Unlike some connectors, Notion requires you to register your own public integration in the Notion Developer Hub
and supply your own Client ID and Secret. This means your users see your app name on the consent screen.
Register the Scalekit redirect URI in your Notion integration's OAuth settings, then paste your credentials into the
Scalekit dashboard. Token management stays fully handled.
Already have credentials?
Calling Notion
4 Calling Notion: What your agent writes
With the connected account active, your agent calls Notion actions using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval and request construction.
Search the workspace
Find pages and databases by keyword across everything the user shared with the integration. Use start_cursor for pagination.
result = actions.execute_tool(
identifier="user_notion_456",
tool_name="notion_data_fetch",
tool_input={
"query": "Q3 roadmap",
"page_size": 10
}
)
# Returns pages and databases matching the query,
# limited to content the user shared with the integration.
Query a database
Fetch rows from a specific database with optional sorting. Extract the database_id from the Notion URL — the last 32 characters, formatted as a hyphenated UUID (8-4-4-4-12).
result = actions.execute_tool(
identifier="user_notion_456",
tool_name="notion_database_query",
tool_input={
"database_id": "2561ab6c-418b-8072-beec-c4779fa811cf",
"page_size": 50,
"sorts": [
{
"property": "Due Date",
"direction": "ascending"
}
]
}
)
Insert a row into a database
Add a new page (row) to an existing database. Call notion_database_fetch first to confirm exact property names — they are case-sensitive. For title fields, always use "title" as the key.
result = actions.execute_tool(
identifier="user_notion_456",
tool_name="notion_database_insert_row",
tool_input={
"database_id": "2561ab6c-418b-8072-beec-c4779fa811cf",
"properties": {
"title": {
"title": [{"text": {"content": "Q3 Roadmap Review"}}]
},
"Status": {"select": {"name": "Not Started"}},
"Due Date": {"date": {"start": "2026-07-15"}},
"Priority": {"select": {"name": "High"}}
}
}
)
Create a standalone page
Create a new child page under an existing parent. Provide parent_page_id and optional child_blocks for body content.
result = actions.execute_tool(
identifier="user_notion_456",
tool_name="notion_page_create",
tool_input={
"parent_page_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"properties": {
"title": {
"title": [{"text": {"content": "Meeting Notes — 2026-07-01"}}]
}
},
"child_blocks": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{"type": "text", "text": {"content": "Action items from today's sync."}}]
}
}
]
}
)
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to look up or write; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
notion_tools = get_tools(
connection_name="notion",
identifier="user_notion_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a knowledge assistant. Use the available tools to help search and manage Notion content."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), notion_tools, prompt)
result = AgentExecutor(agent=agent, tools=notion_tools).invoke({
"input": "Find all high-priority tasks in the Projects database due this month"
})
Other frameworks supported
Tool reference
All 10 Notion tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
Full-text search across the workspace using the Notion /search endpoint. Supports pagination via start_cursor
Create a page inside a database (as a row) or as a child of an existing page. Accepts properties, child blocks, icon, and cover
Create a new database under a parent page with a fully defined property schema
Retrieve a database's title, properties, and full schema definition by ID
notion_database_insert_row
Insert a new row (page) into a database. Property keys must match the database schema exactly — case-sensitive
notion_database_property_retrieve
Query a database and return only specific properties by property ID — reduces payload for large databases
Query database rows with optional sorting and pagination. Returns pages with all property values
Create a top-level comment on a page or reply within an existing discussion thread
Retrieve a single comment by its ID
Fetch all comments for a given block or page. Supports pagination
Connector notes
Notion-specific behavior
BYOC is required — no managed app available
Unlike some Scalekit connectors, Notion requires you to register your own public integration in the Notion Developer Hub,
configure the redirect URI, and supply your Client ID and Secret in the Scalekit dashboard before the authorization flow will work.
Agent access is limited to pages the user explicitly selected
During OAuth, Notion shows a page picker where the user selects which pages and databases to share. Your agent can only
access those resources. API calls to anything outside that selection return a "page not found" error — not a permissions error.
Communicate to users which pages and databases your agent needs before they complete the authorization flow.
Infrastructure decision
Why not build this yourself
The Notion OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Registering and maintaining a public Notion integration with required legal pages, passing Notion's review, and managing OAuth app credentials separately from your application secrets
PROBLEM 02
Capability management — Notion's integration capabilities are set at registration time, not at auth time. Adding new capabilities after users have authorized does not update existing tokens
PROBLEM 03
Per-user token isolation across a multi-tenant system — one user's Notion access token must never be used for another user's workspace
PROBLEM 04
Revocation detection when users disconnect your integration from Notion's Settings — and proactive token refresh before expiry, not after a failed call
That's one connector. Your agent product will eventually need Salesforce, Gmail, HubSpot, Slack, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.